From bb84f0ebc88620050dec7cf4be6283f5cba8b920 Mon Sep 17 00:00:00 2001 From: eugene Date: Wed, 21 Oct 2020 13:34:05 -0400 Subject: [PATCH] multi: store KeyLocator in OpenChannel, use ECDH This commit adds a RevocationKeyLocator field to the OpenChannel struct so that the SCB derivation doesn't have to brute-force the sha chain root key and match the public key. ECDH derivation is now used to derive the key instead of regular private key derivation a la DerivePrivKey. The legacy can still be used to recover old channels. --- chanbackup/single.go | 62 ++++++++++++++++++------------- channeldb/channel.go | 33 +++++++++++++++++ chanrestore.go | 66 +++++++++++++++++++++++++-------- lnwallet/reservation.go | 5 +++ lnwallet/revocation_producer.go | 51 +++++++++++++++++++++++++ lnwallet/wallet.go | 24 ++++-------- 6 files changed, 183 insertions(+), 58 deletions(-) create mode 100644 lnwallet/revocation_producer.go diff --git a/chanbackup/single.go b/chanbackup/single.go index ab8bdcdc6..e51700c88 100644 --- a/chanbackup/single.go +++ b/chanbackup/single.go @@ -124,19 +124,34 @@ type Single struct { func NewSingle(channel *channeldb.OpenChannel, nodeAddrs []net.Addr) Single { - // TODO(roasbeef): update after we start to store the KeyLoc for - // shachain root + var shaChainRootDesc keychain.KeyDescriptor - // We'll need to obtain the shachain root which is derived directly - // from a private key in our keychain. - var b bytes.Buffer - channel.RevocationProducer.Encode(&b) // Can't return an error. + // If the channel has a populated RevocationKeyLocator, then we can + // just store that instead of the public key. + if channel.RevocationKeyLocator.Family == keychain.KeyFamilyRevocationRoot { + shaChainRootDesc = keychain.KeyDescriptor{ + KeyLocator: channel.RevocationKeyLocator, + } + } else { + // If the RevocationKeyLocator is not populated, then we'll need + // to obtain a public point for the shachain root and store that. + // This is the legacy scheme. + var b bytes.Buffer + _ = channel.RevocationProducer.Encode(&b) // Can't return an error. - // Once we have the root, we'll make a public key from it, such that - // the backups plaintext don't carry any private information. When we - // go to recover, we'll present this in order to derive the private - // key. - _, shaChainPoint := btcec.PrivKeyFromBytes(btcec.S256(), b.Bytes()) + // Once we have the root, we'll make a public key from it, such that + // the backups plaintext don't carry any private information. When + // we go to recover, we'll present this in order to derive the + // private key. + _, shaChainPoint := btcec.PrivKeyFromBytes(btcec.S256(), b.Bytes()) + + shaChainRootDesc = keychain.KeyDescriptor{ + PubKey: shaChainPoint, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamilyRevocationRoot, + }, + } + } // If a channel is unconfirmed, the block height of the ShortChannelID // is zero. This will lead to problems when trying to restore that @@ -149,21 +164,16 @@ func NewSingle(channel *channeldb.OpenChannel, } single := Single{ - IsInitiator: channel.IsInitiator, - ChainHash: channel.ChainHash, - FundingOutpoint: channel.FundingOutpoint, - ShortChannelID: chanID, - RemoteNodePub: channel.IdentityPub, - Addresses: nodeAddrs, - Capacity: channel.Capacity, - LocalChanCfg: channel.LocalChanCfg, - RemoteChanCfg: channel.RemoteChanCfg, - ShaChainRootDesc: keychain.KeyDescriptor{ - PubKey: shaChainPoint, - KeyLocator: keychain.KeyLocator{ - Family: keychain.KeyFamilyRevocationRoot, - }, - }, + IsInitiator: channel.IsInitiator, + ChainHash: channel.ChainHash, + FundingOutpoint: channel.FundingOutpoint, + ShortChannelID: chanID, + RemoteNodePub: channel.IdentityPub, + Addresses: nodeAddrs, + Capacity: channel.Capacity, + LocalChanCfg: channel.LocalChanCfg, + RemoteChanCfg: channel.RemoteChanCfg, + ShaChainRootDesc: shaChainRootDesc, } switch { diff --git a/channeldb/channel.go b/channeldb/channel.go index 0d27c0eab..9d8e0199a 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -194,6 +194,10 @@ const ( // A tlv type definition used to serialize an outpoint's indexStatus // for use in the outpoint index. indexStatusType tlv.Type = 0 + + // A tlv type definition used to serialize and deserialize a KeyLocator + // from the database. + keyLocType tlv.Type = 1 ) // indexStatus is an enum-like type that describes what state the @@ -719,6 +723,11 @@ type OpenChannel struct { // was a revocation (true) or a commitment signature (false). LastWasRevoke bool + // RevocationKeyLocator stores the KeyLocator information that we will + // need to derive the shachain root for this channel. This allows us to + // have private key isolation from lnd. + RevocationKeyLocator keychain.KeyLocator + // TODO(roasbeef): eww Db *DB @@ -3286,6 +3295,20 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { return err } + // Write the RevocationKeyLocator as the first entry in a tlv stream. + keyLocRecord := MakeKeyLocRecord( + keyLocType, &channel.RevocationKeyLocator, + ) + + tlvStream, err := tlv.NewStream(keyLocRecord) + if err != nil { + return err + } + + if err := tlvStream.Encode(&w); err != nil { + return err + } + if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil { return err } @@ -3475,6 +3498,16 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { } } + keyLocRecord := MakeKeyLocRecord(keyLocType, &channel.RevocationKeyLocator) + tlvStream, err := tlv.NewStream(keyLocRecord) + if err != nil { + return err + } + + if err := tlvStream.Decode(r); err != nil { + return err + } + channel.Packager = NewChannelPackager(channel.ShortChannelID) // Finally, read the optional shutdown scripts. diff --git a/chanrestore.go b/chanrestore.go index a309866c5..7527499cd 100644 --- a/chanrestore.go +++ b/chanrestore.go @@ -48,20 +48,7 @@ type chanDBRestorer struct { func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) ( *channeldb.ChannelShell, error) { - // First, we'll also need to obtain the private key for the shachain - // root from the encoded public key. - // - // TODO(roasbeef): now adds req for hardware signers to impl - // shachain... - privKey, err := c.secretKeys.DerivePrivKey(backup.ShaChainRootDesc) - if err != nil { - return nil, fmt.Errorf("unable to derive shachain root key: %v", err) - } - revRoot, err := chainhash.NewHash(privKey.Serialize()) - if err != nil { - return nil, err - } - shaChainProducer := shachain.NewRevocationProducer(*revRoot) + var err error // Each of the keys in our local channel config only have their // locators populate, so we'll re-derive the raw key now as we'll need @@ -97,6 +84,53 @@ func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) ( return nil, fmt.Errorf("unable to derive htlc key: %v", err) } + // The shachain root that seeds RevocationProducer for this channel. + // It currently has two possible formats. + var revRoot *chainhash.Hash + + // If the PubKey field is non-nil, then this shachain root is using the + // legacy non-ECDH scheme. + if backup.ShaChainRootDesc.PubKey != nil { + ltndLog.Debugf("Using legacy revocation producer format for "+ + "channel point %v", backup.FundingOutpoint) + + // Obtain the private key for the shachain root from the + // encoded public key. + privKey, err := c.secretKeys.DerivePrivKey( + backup.ShaChainRootDesc, + ) + if err != nil { + return nil, fmt.Errorf("could not derive private key "+ + "for legacy channel revocation root format: "+ + "%v", err) + } + + revRoot, err = chainhash.NewHash(privKey.Serialize()) + if err != nil { + return nil, err + } + } else { + ltndLog.Debugf("Using new ECDH revocation producer format "+ + "for channel point %v", backup.FundingOutpoint) + + // This is the scheme in which the shachain root is derived via + // an ECDH operation on the private key of ShaChainRootDesc and + // our public multisig key. + ecdh, err := c.secretKeys.ECDH( + backup.ShaChainRootDesc, + backup.LocalChanCfg.MultiSigKey.PubKey, + ) + if err != nil { + return nil, fmt.Errorf("unable to derive shachain "+ + "root: %v", err) + } + + ch := chainhash.Hash(ecdh) + revRoot = &ch + } + + shaChainProducer := shachain.NewRevocationProducer(*revRoot) + var chanType channeldb.ChannelType switch backup.Version { @@ -119,8 +153,8 @@ func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) ( return nil, fmt.Errorf("unknown Single version: %v", err) } - ltndLog.Infof("SCB Recovery: created channel shell for ChannelPoint(%v), "+ - "chan_type=%v", backup.FundingOutpoint, chanType) + ltndLog.Infof("SCB Recovery: created channel shell for ChannelPoint"+ + "(%v), chan_type=%v", backup.FundingOutpoint, chanType) chanShell := channeldb.ChannelShell{ NodeAddrs: backup.Addresses, diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index a5aca0ffb..2b24178c4 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" @@ -161,6 +162,10 @@ type ChannelReservation struct { chanFunder chanfunding.Assembler fundingIntent chanfunding.Intent + + // nextRevocationKeyLoc stores the key locator information for this + // channel. + nextRevocationKeyLoc keychain.KeyLocator } // NewChannelReservation creates a new channel reservation. This function is diff --git a/lnwallet/revocation_producer.go b/lnwallet/revocation_producer.go new file mode 100644 index 000000000..6f8c56c55 --- /dev/null +++ b/lnwallet/revocation_producer.go @@ -0,0 +1,51 @@ +// +build !rpctest + +package lnwallet + +import ( + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/shachain" +) + +// nextRevocationProducer creates a new revocation producer, deriving the +// revocation root by applying ECDH to a new key from our revocation root family +// and the multisig key we use for the channel. +func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, + keyRing keychain.KeyRing) (shachain.Producer, error) { + + // Derive the next key in the revocation root family. + nextRevocationKeyDesc, err := keyRing.DeriveNextKey( + keychain.KeyFamilyRevocationRoot, + ) + if err != nil { + return nil, err + } + + // If the DeriveNextKey call returns the first key with Index 0, we need + // to re-derive the key as the keychain/btcwallet.go DerivePrivKey call + // special-cases Index 0. + if nextRevocationKeyDesc.Index == 0 { + nextRevocationKeyDesc, err = keyRing.DeriveNextKey( + keychain.KeyFamilyRevocationRoot, + ) + if err != nil { + return nil, err + } + } + + res.nextRevocationKeyLoc = nextRevocationKeyDesc.KeyLocator + + // Perform an ECDH operation between the private key described in + // nextRevocationKeyDesc and our public multisig key. The result will be + // used to seed the revocation producer. + revRoot, err := l.ECDH( + nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey, + ) + if err != nil { + return nil, err + } + + // Once we have the root, we can then generate our shachain producer + // and from that generate the per-commitment point. + return shachain.NewRevocationProducer(revRoot), nil +} diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index e550d1e86..3c3076466 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1071,26 +1071,13 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, return err } - // With the above keys created, we'll also need to initialization our - // initial revocation tree state. - nextRevocationKeyDesc, err := keyRing.DeriveNextKey( - keychain.KeyFamilyRevocationRoot, - ) - if err != nil { - return err - } - revocationRoot, err := l.DerivePrivKey(nextRevocationKeyDesc) + // With the above keys created, we'll also need to initialize our + // revocation tree state, and from that generate the per-commitment point. + producer, err := l.nextRevocationProducer(reservation, keyRing) if err != nil { return err } - // Once we have the root, we can then generate our shachain producer - // and from that generate the per-commitment point. - revRoot, err := chainhash.NewHash(revocationRoot.Serialize()) - if err != nil { - return err - } - producer := shachain.NewRevocationProducer(*revRoot) firstPreimage, err := producer.AtIndex(0) if err != nil { return err @@ -1727,6 +1714,8 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs res.partialState.RemoteShutdownScript = res.theirContribution.UpfrontShutdown + res.partialState.RevocationKeyLocator = res.nextRevocationKeyLoc + // Add the complete funding transaction to the DB, in its open bucket // which will be used for the lifetime of this channel. nodeAddr := res.nodeAddr @@ -1891,6 +1880,9 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { // which will be used for the lifetime of this channel. chanState.LocalChanCfg = pendingReservation.ourContribution.toChanConfig() chanState.RemoteChanCfg = pendingReservation.theirContribution.toChanConfig() + + chanState.RevocationKeyLocator = pendingReservation.nextRevocationKeyLoc + err = chanState.SyncPending(pendingReservation.nodeAddr, uint32(bestHeight)) if err != nil { req.err <- err