Skip to content

Commit b141033

Browse files
authored
Merge pull request #154 from lightninglabs/zombierecovery-cln
zombierecovery: make compatible with CLN
2 parents e7f7823 + ac43d49 commit b141033

13 files changed

+645
-157
lines changed

cln/derivation.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package cln
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/binary"
6+
"fmt"
7+
8+
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/lightningnetwork/lnd/keychain"
10+
"golang.org/x/crypto/hkdf"
11+
)
12+
13+
const (
14+
KeyOffsetFunding = 0
15+
KeyOffsetRevocation = 1
16+
KeyOffsetHtlc = 2
17+
KeyOffsetPayment = 3
18+
KeyOffsetDelayed = 4
19+
)
20+
21+
var (
22+
InfoNodeID = []byte("nodeid")
23+
InfoPeerSeed = []byte("peer seed")
24+
InfoPerPeer = []byte("per-peer seed")
25+
InfoCLightning = []byte("c-lightning")
26+
)
27+
28+
// NodeKey derives a CLN node key from the given HSM secret.
29+
func NodeKey(hsmSecret [32]byte) (*btcec.PublicKey, *btcec.PrivateKey, error) {
30+
salt := make([]byte, 4)
31+
privKeyBytes, err := HkdfSha256(hsmSecret[:], salt, InfoNodeID)
32+
if err != nil {
33+
return nil, nil, err
34+
}
35+
36+
privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes[:])
37+
return pubKey, privKey, nil
38+
}
39+
40+
// DeriveKeyPair derives a channel key pair from the given HSM secret, and the
41+
// key descriptor. The public key in the key descriptor is used as the peer's
42+
// public key, the family is converted to the CLN key type, and the index is
43+
// used as the channel's database index.
44+
func DeriveKeyPair(hsmSecret [32]byte,
45+
desc *keychain.KeyDescriptor) (*btcec.PublicKey, *btcec.PrivateKey,
46+
error) {
47+
48+
var offset int
49+
switch desc.Family {
50+
case keychain.KeyFamilyMultiSig:
51+
offset = KeyOffsetFunding
52+
53+
case keychain.KeyFamilyRevocationBase:
54+
offset = KeyOffsetRevocation
55+
56+
case keychain.KeyFamilyHtlcBase:
57+
offset = KeyOffsetHtlc
58+
59+
case keychain.KeyFamilyPaymentBase:
60+
offset = KeyOffsetPayment
61+
62+
case keychain.KeyFamilyDelayBase:
63+
offset = KeyOffsetDelayed
64+
65+
case keychain.KeyFamilyNodeKey:
66+
return NodeKey(hsmSecret)
67+
68+
default:
69+
return nil, nil, fmt.Errorf("unsupported key family for CLN: "+
70+
"%v", desc.Family)
71+
}
72+
73+
channelBase, err := HkdfSha256(hsmSecret[:], nil, InfoPeerSeed)
74+
if err != nil {
75+
return nil, nil, err
76+
}
77+
78+
peerAndChannel := make([]byte, 33+8)
79+
copy(peerAndChannel[:33], desc.PubKey.SerializeCompressed())
80+
binary.LittleEndian.PutUint64(peerAndChannel[33:], uint64(desc.Index))
81+
82+
channelSeed, err := HkdfSha256(
83+
channelBase[:], peerAndChannel, InfoPerPeer,
84+
)
85+
if err != nil {
86+
return nil, nil, err
87+
}
88+
89+
fundingKey, err := HkdfSha256WithSkip(
90+
channelSeed[:], nil, InfoCLightning, offset*32,
91+
)
92+
if err != nil {
93+
return nil, nil, err
94+
}
95+
96+
privKey, pubKey := btcec.PrivKeyFromBytes(fundingKey[:])
97+
return pubKey, privKey, nil
98+
}
99+
100+
// HkdfSha256 derives a 32-byte key from the given input key material, salt, and
101+
// info using the HKDF-SHA256 key derivation function.
102+
func HkdfSha256(key, salt, info []byte) ([32]byte, error) {
103+
return HkdfSha256WithSkip(key, salt, info, 0)
104+
}
105+
106+
// HkdfSha256WithSkip derives a 32-byte key from the given input key material,
107+
// salt, and info using the HKDF-SHA256 key derivation function and skips the
108+
// first `skip` bytes of the output.
109+
func HkdfSha256WithSkip(key, salt, info []byte, skip int) ([32]byte, error) {
110+
expander := hkdf.New(sha256.New, key, salt, info)
111+
112+
if skip > 0 {
113+
skippedBytes := make([]byte, skip)
114+
_, err := expander.Read(skippedBytes)
115+
if err != nil {
116+
return [32]byte{}, err
117+
}
118+
}
119+
120+
var outputKey [32]byte
121+
_, err := expander.Read(outputKey[:])
122+
if err != nil {
123+
return [32]byte{}, err
124+
}
125+
126+
return outputKey, nil
127+
}

cln/derivation_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package cln
2+
3+
import (
4+
"encoding/hex"
5+
"testing"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/lightningnetwork/lnd/keychain"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
var (
13+
hsmSecret = [32]byte{
14+
0x3f, 0x0a, 0x06, 0xc6, 0x38, 0x5b, 0x74, 0x93,
15+
0xf7, 0x5a, 0xa0, 0x08, 0x9f, 0x31, 0x6a, 0x13,
16+
0xbf, 0x72, 0xbe, 0xb4, 0x30, 0xe5, 0x9e, 0x71,
17+
0xb5, 0xac, 0x5a, 0x73, 0x58, 0x1a, 0x62, 0x70,
18+
}
19+
nodeKeyBytes, _ = hex.DecodeString(
20+
"035149629152c1bee83f1e148a51400b5f24bf3e2ca53384dd801418446e" +
21+
"1f53fe",
22+
)
23+
24+
peerPubKeyBytes, _ = hex.DecodeString(
25+
"02678187ca43e6a6f62f9185be98a933bf485313061e6a05578bbd83c54e" +
26+
"88d460",
27+
)
28+
peerPubKey, _ = btcec.ParsePubKey(peerPubKeyBytes)
29+
30+
expectedFundingKeyBytes, _ = hex.DecodeString(
31+
"0326a2171c97673cc8cd7a04a043f0224c59591fc8c9de320a48f7c9b68a" +
32+
"b0ae2b",
33+
)
34+
)
35+
36+
func TestNodeKey(t *testing.T) {
37+
nodeKey, _, err := NodeKey(hsmSecret)
38+
require.NoError(t, err)
39+
40+
require.Equal(t, nodeKeyBytes, nodeKey.SerializeCompressed())
41+
}
42+
43+
func TestFundingKey(t *testing.T) {
44+
fundingKey, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{
45+
PubKey: peerPubKey,
46+
KeyLocator: keychain.KeyLocator{
47+
Family: keychain.KeyFamilyMultiSig,
48+
Index: 1,
49+
},
50+
})
51+
require.NoError(t, err)
52+
53+
require.Equal(
54+
t, expectedFundingKeyBytes, fundingKey.SerializeCompressed(),
55+
)
56+
}
57+
58+
func TestPaymentBasePointSecret(t *testing.T) {
59+
hsmSecret2, _ := hex.DecodeString(
60+
"665b09e6fc86391f0141d957eb14ec30f8f8a58a876842792474cacc2448" +
61+
"9456",
62+
)
63+
64+
basePointPeerBytes, _ := hex.DecodeString(
65+
"0350aeef9f33a157953d3c3c1ef464bdf421204461959524e52e530c17f1" +
66+
"66f541",
67+
)
68+
69+
expectedPaymentBasePointBytes, _ := hex.DecodeString(
70+
"0339c93ca896829672510f8a4e51caef4b5f6a26f880acf5a120725a7f02" +
71+
"7b56b4",
72+
)
73+
74+
var hsmSecret [32]byte
75+
copy(hsmSecret[:], hsmSecret2)
76+
77+
basepointPeer, err := btcec.ParsePubKey(basePointPeerBytes)
78+
require.NoError(t, err)
79+
80+
nk, _, err := NodeKey(hsmSecret)
81+
require.NoError(t, err)
82+
83+
t.Logf("Node key: %x", nk.SerializeCompressed())
84+
85+
fk, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{
86+
PubKey: basepointPeer,
87+
KeyLocator: keychain.KeyLocator{
88+
Family: keychain.KeyFamilyMultiSig,
89+
Index: 1,
90+
},
91+
})
92+
require.NoError(t, err)
93+
94+
t.Logf("Funding key: %x", fk.SerializeCompressed())
95+
96+
paymentBasePoint, _, err := DeriveKeyPair(
97+
hsmSecret, &keychain.KeyDescriptor{
98+
PubKey: basepointPeer,
99+
KeyLocator: keychain.KeyLocator{
100+
Family: keychain.KeyFamilyPaymentBase,
101+
Index: 1,
102+
},
103+
},
104+
)
105+
require.NoError(t, err)
106+
107+
require.Equal(
108+
t, expectedPaymentBasePointBytes,
109+
paymentBasePoint.SerializeCompressed(),
110+
)
111+
}

cln/signer.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package cln
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/btcsuite/btcd/btcutil/psbt"
9+
"github.com/btcsuite/btcd/txscript"
10+
"github.com/btcsuite/btcd/wire"
11+
"github.com/btcsuite/btcwallet/wallet"
12+
"github.com/lightninglabs/chantools/lnd"
13+
"github.com/lightningnetwork/lnd/input"
14+
"github.com/lightningnetwork/lnd/keychain"
15+
)
16+
17+
type Signer struct {
18+
HsmSecret [32]byte
19+
}
20+
21+
func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
22+
signDesc *input.SignDescriptor) (input.Signature, error) {
23+
24+
// First attempt to fetch the private key which corresponds to the
25+
// specified public key.
26+
privKey, err := s.FetchPrivateKey(&signDesc.KeyDesc)
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
return lnd.SignOutputRawWithPrivateKey(tx, signDesc, privKey)
32+
}
33+
34+
func (s *Signer) FetchPrivateKey(
35+
descriptor *keychain.KeyDescriptor) (*btcec.PrivateKey, error) {
36+
37+
_, privKey, err := DeriveKeyPair(s.HsmSecret, descriptor)
38+
return privKey, err
39+
}
40+
41+
func (s *Signer) FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey,
42+
maxNumKeys uint32) (*keychain.KeyDescriptor, error) {
43+
44+
// Loop through the local multisig keys to find the target key.
45+
for index := range maxNumKeys {
46+
privKey, err := s.FetchPrivateKey(&keychain.KeyDescriptor{
47+
PubKey: peerPubKey,
48+
KeyLocator: keychain.KeyLocator{
49+
Family: keychain.KeyFamilyMultiSig,
50+
Index: index,
51+
},
52+
})
53+
if err != nil {
54+
return nil, fmt.Errorf("error deriving funding "+
55+
"private key: %w", err)
56+
}
57+
58+
currentPubkey := privKey.PubKey()
59+
if !targetPubkey.IsEqual(currentPubkey) {
60+
continue
61+
}
62+
63+
return &keychain.KeyDescriptor{
64+
PubKey: peerPubKey,
65+
KeyLocator: keychain.KeyLocator{
66+
Family: keychain.KeyFamilyMultiSig,
67+
Index: index,
68+
},
69+
}, nil
70+
}
71+
72+
return nil, errors.New("no matching pubkeys found")
73+
}
74+
75+
func (s *Signer) AddPartialSignature(packet *psbt.Packet,
76+
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte,
77+
inputIndex int) error {
78+
79+
// Now we add our partial signature.
80+
prevOutFetcher := wallet.PsbtPrevOutputFetcher(packet)
81+
signDesc := &input.SignDescriptor{
82+
KeyDesc: keyDesc,
83+
WitnessScript: witnessScript,
84+
Output: utxo,
85+
InputIndex: inputIndex,
86+
HashType: txscript.SigHashAll,
87+
PrevOutputFetcher: prevOutFetcher,
88+
SigHashes: txscript.NewTxSigHashes(
89+
packet.UnsignedTx, prevOutFetcher,
90+
),
91+
}
92+
ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc)
93+
if err != nil {
94+
return fmt.Errorf("error signing with our key: %w", err)
95+
}
96+
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))
97+
98+
// Because of the way we derive keys in CLN, the public key in the key
99+
// descriptor is the peer's public key, not our own. So we need to
100+
// derive our own public key from the private key.
101+
ourPrivKey, err := s.FetchPrivateKey(&keyDesc)
102+
if err != nil {
103+
return fmt.Errorf("error fetching private key for descriptor "+
104+
"%v: %w", keyDesc, err)
105+
}
106+
ourPubKey := ourPrivKey.PubKey()
107+
108+
// Great, we were able to create our sig, let's add it to the PSBT.
109+
updater, err := psbt.NewUpdater(packet)
110+
if err != nil {
111+
return fmt.Errorf("error creating PSBT updater: %w", err)
112+
}
113+
status, err := updater.Sign(
114+
inputIndex, ourSig, ourPubKey.SerializeCompressed(), nil,
115+
witnessScript,
116+
)
117+
if err != nil {
118+
return fmt.Errorf("error adding signature to PSBT: %w", err)
119+
}
120+
if status != 0 {
121+
return fmt.Errorf("unexpected status for signature update, "+
122+
"got %d wanted 0", status)
123+
}
124+
125+
return nil
126+
}
127+
128+
var _ lnd.ChannelSigner = (*Signer)(nil)

0 commit comments

Comments
 (0)