cln+lnd+zombierecovery: make fully CLN compatible

This commit is contained in:
Oliver Gugger 2025-06-17 17:33:10 +02:00
parent 9fa5f46c6b
commit 78f1fe8f31
No known key found for this signature in database
GPG key ID: 8E4256593F177720
4 changed files with 175 additions and 84 deletions

View file

@ -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)

View file

@ -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!

View file

@ -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
}

View file

@ -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)