diff --git a/chanbackup/single.go b/chanbackup/single.go index ab8bdcdc..e51700c8 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 d36ded21..9d8e0199 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. @@ -3657,3 +3690,38 @@ func storeThawHeight(chanBucket kvdb.RwBucket, height uint32) error { func deleteThawHeight(chanBucket kvdb.RwBucket) error { return chanBucket.Delete(frozenChanKey) } + +// EKeyLocator is an encoder for keychain.KeyLocator. +func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*keychain.KeyLocator); ok { + err := tlv.EUint32T(w, uint32(v.Family), buf) + if err != nil { + return err + } + + return tlv.EUint32T(w, v.Index, buf) + } + return tlv.NewTypeForEncodingErr(val, "keychain.KeyLocator") +} + +// DKeyLocator is a decoder for keychain.KeyLocator. +func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*keychain.KeyLocator); ok { + var family uint32 + err := tlv.DUint32(r, &family, buf, 4) + if err != nil { + return err + } + v.Family = keychain.KeyFamily(family) + + return tlv.DUint32(r, &v.Index, buf, 4) + } + return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) +} + +// MakeKeyLocRecord creates a Record out of a KeyLocator using the passed +// Type and the EKeyLocator and DKeyLocator functions. The size will always be +// 8 as KeyFamily is uint32 and the Index is uint32. +func MakeKeyLocRecord(typ tlv.Type, keyLoc *keychain.KeyLocator) tlv.Record { + return tlv.MakeStaticRecord(typ, keyLoc, 8, EKeyLocator, DKeyLocator) +} diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 43747372..482bc7af 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -8,6 +8,8 @@ import ( "runtime" "testing" + "github.com/stretchr/testify/require" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -50,6 +52,9 @@ var ( IP: net.ParseIP("127.0.0.1"), Port: 18555, } + + // keyLocIndex is the KeyLocator Index we use for TestKeyLocatorEncoding. + keyLocIndex = uint32(2049) ) // testChannelParams is a struct which details the specifics of how a channel @@ -1587,3 +1592,33 @@ func TestHasChanStatus(t *testing.T) { }) } } + +// TestKeyLocatorEncoding tests that we are able to serialize a given +// keychain.KeyLocator. After successfully encoding, we check that the decode +// output arrives at the same initial KeyLocator. +func TestKeyLocatorEncoding(t *testing.T) { + keyLoc := keychain.KeyLocator{ + Family: keychain.KeyFamilyRevocationRoot, + Index: keyLocIndex, + } + + // First, we'll encode the KeyLocator into a buffer. + var ( + b bytes.Buffer + buf [8]byte + ) + + err := EKeyLocator(&b, &keyLoc, &buf) + require.NoError(t, err, "unable to encode key locator") + + // Next, we'll attempt to decode the bytes into a new KeyLocator. + r := bytes.NewReader(b.Bytes()) + var decodedKeyLoc keychain.KeyLocator + + err = DKeyLocator(r, &decodedKeyLoc, &buf, 8) + require.NoError(t, err, "unable to decode key locator") + + // Finally, we'll compare that the original KeyLocator and the decoded + // version are equal. + require.Equal(t, keyLoc, decodedKeyLoc) +} diff --git a/chanrestore.go b/chanrestore.go index a309866c..7527499c 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/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go index 3d125cd4..437b924b 100644 --- a/lntest/itest/lnd_channel_backup_test.go +++ b/lntest/itest/lnd_channel_backup_test.go @@ -16,6 +16,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" @@ -356,6 +357,37 @@ func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) { ) }, }, + + // Restore by also creating a channel with the legacy revocation + // producer format to make sure old SCBs can still be recovered. + { + name: "old revocation producer format", + initiator: true, + legacyRevocation: true, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // For this restoration method, we'll grab the + // current multi-channel backup from the old + // node, and use it to restore a new node + // within the closure. + req := &lnrpc.ChanBackupExportRequest{} + chanBackup, err := oldNode.ExportAllChannelBackups( + ctxb, req, + ) + require.NoError(t.t, err) + + multi := chanBackup.MultiChanBackup.MultiChanBackup + + // In our nodeRestorer function, we'll restore + // the node from seed, then manually recover the + // channel backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + ) + }, + }, } // TODO(roasbeef): online vs offline close? @@ -765,6 +797,10 @@ type chanRestoreTestCase struct { // used for the channels created in the test. anchorCommit bool + // legacyRevocation signals if a channel with the legacy revocation + // producer format should also be created before restoring. + legacyRevocation bool + // restoreMethod takes an old node, then returns a function // closure that'll return the same node, but with its state // restored via a custom method. We use this to abstract away @@ -867,6 +903,13 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness, t.Fatalf("channel backup not updated in time: %v", err) } + // Also create channels with the legacy revocation producer format if + // requested. + case testCase.legacyRevocation: + createLegacyRevocationChannel( + net, t, chanAmt, pushAmt, from, to, + ) + default: ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout) chanPoint := openChannelAndAssert( @@ -1068,6 +1111,117 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness, ) } +// createLegacyRevocationChannel creates a single channel using the legacy +// revocation producer format by using PSBT to signal a special pending channel +// ID. +func createLegacyRevocationChannel(net *lntest.NetworkHarness, t *harnessTest, + chanAmt, pushAmt btcutil.Amount, from, to *lntest.HarnessNode) { + + ctxb := context.Background() + + // We'll signal to the wallet that we also want to create a channel with + // the legacy revocation producer format that relies on deriving a + // private key from the key ring. This is only available during itests + // to make sure we don't hard depend on the DerivePrivKey method of the + // key ring. We can signal the wallet by setting a custom pending + // channel ID. To be able to do that, we need to set a funding shim + // which is easiest by using PSBT funding. The ID is the hex + // representation of the string "legacy-revocation". + itestLegacyFormatChanID := [32]byte{ + 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x2d, 0x72, 0x65, 0x76, + 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + } + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + openChannelReq := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + FundingShim: &lnrpc.FundingShim{ + Shim: &lnrpc.FundingShim_PsbtShim{ + PsbtShim: &lnrpc.PsbtShim{ + PendingChanId: itestLegacyFormatChanID[:], + }, + }, + }, + } + chanUpdates, tempPsbt, err := openChannelPsbt( + ctxt, from, to, openChannelReq, + ) + require.NoError(t.t, err) + + // Fund the PSBT by using the source node's wallet. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + fundReq := &walletrpc.FundPsbtRequest{ + Template: &walletrpc.FundPsbtRequest_Psbt{ + Psbt: tempPsbt, + }, + Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ + SatPerVbyte: 2, + }, + } + fundResp, err := from.WalletKitClient.FundPsbt(ctxt, fundReq) + require.NoError(t.t, err) + + // We have a PSBT that has no witness data yet, which is exactly what we + // need for the next step of verifying the PSBT with the funding intents. + _, err = from.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ + PsbtVerify: &lnrpc.FundingPsbtVerify{ + PendingChanId: itestLegacyFormatChanID[:], + FundedPsbt: fundResp.FundedPsbt, + }, + }, + }) + require.NoError(t.t, err) + + // Now we'll ask the source node's wallet to sign the PSBT so we can + // finish the funding flow. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + finalizeReq := &walletrpc.FinalizePsbtRequest{ + FundedPsbt: fundResp.FundedPsbt, + } + finalizeRes, err := from.WalletKitClient.FinalizePsbt( + ctxt, finalizeReq, + ) + require.NoError(t.t, err) + + // We've signed our PSBT now, let's pass it to the intent again. + _, err = from.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ + PsbtFinalize: &lnrpc.FundingPsbtFinalize{ + PendingChanId: itestLegacyFormatChanID[:], + SignedPsbt: finalizeRes.SignedPsbt, + }, + }, + }) + require.NoError(t.t, err) + + // Consume the "channel pending" update. This waits until the funding + // transaction was fully compiled. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + updateResp, err := receiveChanUpdate(ctxt, chanUpdates) + require.NoError(t.t, err) + upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) + require.True(t.t, ok) + chanPoint := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ + FundingTxidBytes: upd.ChanPending.Txid, + }, + OutputIndex: upd.ChanPending.OutputIndex, + } + + _ = mineBlocks(t, net, 6, 1) + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + err = from.WaitForNetworkChannelOpen(ctxt, chanPoint) + require.NoError(t.t, err) + err = to.WaitForNetworkChannelOpen(ctxt, chanPoint) + require.NoError(t.t, err) +} + // chanRestoreViaRPC is a helper test method that returns a nodeRestorer // instance which will restore the target node from a password+seed, then // trigger a SCB restore using the RPC interface. diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index a5aca0ff..2b24178c 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 00000000..6f8c56c5 --- /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/revocation_producer_itest.go b/lnwallet/revocation_producer_itest.go new file mode 100644 index 00000000..6ef89119 --- /dev/null +++ b/lnwallet/revocation_producer_itest.go @@ -0,0 +1,77 @@ +// +build rpctest + +package lnwallet + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "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 + } + + // Within our itests, we want to make sure we can still restore channel + // backups created with the old revocation root derivation method. To + // create a channel in the legacy format during the test, we signal this + // by setting an explicit pending channel ID. The ID is the hex + // representation of the string "legacy-revocation". + itestLegacyFormatChanID := [32]byte{ + 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x2d, 0x72, 0x65, 0x76, + 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + } + if res.pendingChanID == itestLegacyFormatChanID { + revocationRoot, err := l.DerivePrivKey(nextRevocationKeyDesc) + 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. + revRoot, err := chainhash.NewHash(revocationRoot.Serialize()) + if err != nil { + return nil, err + } + + return shachain.NewRevocationProducer(*revRoot), nil + } + + // 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 e550d1e8..3c307646 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