mirror of
https://github.com/guggero/chantools.git
synced 2026-08-13 12:33:34 +02:00
cln+lnd+zombierecovery: make fully CLN compatible
This commit is contained in:
parent
9fa5f46c6b
commit
78f1fe8f31
4 changed files with 175 additions and 84 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue