From 320af437d71cc491fa13a9fa536b84672574cf66 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Wed, 2 Oct 2019 16:43:58 +0200 Subject: [PATCH] Extend funding key path to 256 bits (#1154) Our random funding key path is now 8 * 32 bits plus a 1' (funder) or 0' (fundee). Channel key paths are computed from the sha256 of the funding public key (we take all 256 bits). --- .../fr/acinq/eclair/crypto/KeyManager.scala | 14 +++- .../acinq/eclair/crypto/LocalKeyManager.scala | 10 ++- .../main/scala/fr/acinq/eclair/io/Peer.scala | 5 +- .../eclair/crypto/LocalKeyManagerSpec.scala | 78 ++++++++++++++++++- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala index 017dc44f4..9745d319c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala @@ -54,6 +54,15 @@ trait KeyManager { localParams.fundingKeyPath } + /** + * + * @param isFunder true if we're funding this channel + * @return a partial key path for a new funding public key. This key path will be extended: + * - with a specific "chain" prefix + * - with a specific "funding pubkey" suffix + */ + def newFundingKeyPath(isFunder: Boolean) : DeterministicWallet.KeyPath + /** * * @param tx input transaction @@ -112,9 +121,10 @@ object KeyManager { * @return a BIP32 path */ def channelKeyPath(fundingPubKey: PublicKey) : DeterministicWallet.KeyPath = { - val buffer = fundingPubKey.hash160.take(16) + val buffer = Crypto.sha256(fundingPubKey.value) val bis = new ByteArrayInputStream(buffer.toArray) - DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN))) + def next() = Protocol.uint32(bis, ByteOrder.BIG_ENDIAN) + DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next())) } def channelKeyPath(fundingPubKey: DeterministicWallet.ExtendedPublicKey) : DeterministicWallet.KeyPath = channelKeyPath(fundingPubKey.publicKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala index f4aaea07d..d7945b71a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala @@ -17,13 +17,13 @@ package fr.acinq.eclair.crypto import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} -import fr.acinq.bitcoin.Crypto.{PublicKey, PrivateKey} +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.{derivePrivateKey, _} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} -import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo +import fr.acinq.eclair.{ShortChannelId, secureRandom} import scodec.bits.ByteVector object LocalKeyManager { @@ -80,6 +80,12 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath) = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.value :+ 1.toByte) + override def newFundingKeyPath(isFunder: Boolean): KeyPath = { + val last = DeterministicWallet.hardened(if (isFunder) 1 else 0) + def next() = secureRandom.nextInt() & 0xFFFFFFFFL + DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next(), last)) + } + override def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(0))) override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(1))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index af9a02172..bad45c068 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{secureRandom, wire, _} +import fr.acinq.eclair.{wire, _} import kamon.Kamon import scodec.Attempt import scodec.bits.ByteVector @@ -663,8 +663,7 @@ object Peer { def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { // we make sure that funder and fundee key path end differently - val last = DeterministicWallet.hardened(if (isFunder) 1 else 0) - val fundingKeyPath = DeterministicWallet.KeyPath(Seq(secureRandom.nextInt() & 0xFFFFFFFFL, secureRandom.nextInt() & 0xFFFFFFFFL, last)) + val fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isFunder) makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingAmount, fundingKeyPath) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala index fa2f87240..4cb96ea7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala @@ -19,6 +19,8 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.KeyPath import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} +import fr.acinq.eclair.TestConstants +import fr.acinq.eclair.channel.ChannelVersion import org.scalatest.FunSuite import scodec.bits._ @@ -63,6 +65,80 @@ class LocalKeyManagerSpec extends FunSuite { // will break existing channels ! val pub = PrivateKey(ByteVector32.fromValidHex("01" * 32)).publicKey val keyPath = KeyManager.channelKeyPath(pub) - assert(keyPath.toString() == "m/2041577608/1982247572/689197082'/1288840885") + assert(keyPath.toString() == "m/1909530642'/1080788911/847211985'/1791010671/1303008749'/34154019'/723973395/767609665") + } + + def makefundingKeyPath(entropy: ByteVector, isFunder: Boolean) = { + val items = for(i <- 0 to 7) yield entropy.drop(i * 4).take(4).toInt(signed = false) & 0xFFFFFFFFL + val last = DeterministicWallet.hardened(if (isFunder) 1L else 0L) + KeyPath(items :+ last) + } + + test("test vectors (testnet, funder)") { + val seed = ByteVector.fromValidHex("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501") + val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isFunder = true) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"216414970b4216b197a1040367419ad6922f80e8b73ced083e9afe5e6ddd8e4c").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"a4e7ab3c54752a3487b3c474467843843f28d3bb9113e65e92056ad45d1e318e").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"de24c43d24b8d6bc66b020ac81164206bb577c7924511d4e99431c0d60505012").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"8aa7b8b14a7035540c331c030be0dd73e8806fb0c97a2519d63775c2f579a950").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"94eca6eade204d6e753344c347b46bb09067c92b2fe371cf4f8362c1594c8c59").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("64e9d1e9840add3bb02c1525995edd28feea67f1df7a9ee075179e8541adc7a2"), 0xFFFFFFFFFFFFL)) + } + + test("test vectors (testnet, fundee)") { + val seed = ByteVector.fromValidHex("aeb3e9b5642cd4523e9e09164047f60adb413633549c3c6189192921311894d501") + val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isFunder = false) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"7bb8019c99fcba1c6bd0cc7f3c635c14c658d26751232d6a6350d8b6127d53c3").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"26510db99546c9b08418fe9df2da710a92afa6cc4e5681141610dfb8019052e6").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"0766c93fd06f69287fcc7b343916e678b83942345d4080e83f4c8a061b1a9f4b").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"094aa052a9647228fd80e42461cae26c04f6cdd1665b816d4660df686915319a").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"8ec62bd03b241a2e522477ae1a9861a668429ab3e443abd2aa0f2f10e2dc2206").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("c49e98202b0fee19f28fd3af60691aaacdd2c09e20896f5fa3ad1b9b70e4879f"), 0xFFFFFFFFFFFFL)) + } + + test("test vectors (mainnet, funder)") { + val seed = ByteVector.fromValidHex("d8d5431487c2b19ee6486aad6c3bdfb99d10b727bade7fa848e2ab7901c15bff01") + val keyManager = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isFunder = true) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"b97c04796850e9d74a06c9d7230d85e2ecca3598b162ddf902895ece820c8f09").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"ee13db7f2d7e672f21395111ee169af8462c6e8d1a6a78d808f7447b27155ffb").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"7fc18e4c925bf3c5a83411eac7f234f0c5eaef9a8022b22ec6e3272ae329e17e").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"c0d9a3e3601d79b11b948db9d672fcddafcb9a3c0873c6a738bb09087ea2bfc6").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"bd3ba7068d131a9ab47f33202d532c5824cc5fc35a9adada3644ac2994372228").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("7799de34239f97837a12191f5b60e766e32e9704bb84b0f12b539e9bf6a0dc2a"), 0xFFFFFFFFFFFFL)) + } + + test("test vectors (mainnet, fundee)") { + val seed = ByteVector.fromValidHex("4b809dd593b36131c454d60c2f7bdfd49d12ec455e5b657c47a9ca0f5dfc5eef01") + val keyManager = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isFunder = false) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"46a4e818615a48a99ce9f6bd73eea07d5822dcfcdff18081ea781d4e5e6c036c").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"c2cd9e2f9f8203f16b1751bd252285bb2e7fc4688857d620467b99645ebdfbe6").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"1e4d3527788b39dc8ebc0ae6368a67e92eff55a43bea8e93054338ca850fa340").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"6bc30b0852fbc653451662a1ff6ad530f311d58b5e5661b541eb57dba8206937").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"b1be27b5232e3bc5d6a261949b4ee68d96fa61f481998d36342e2ad99444cf8a").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("eeb3bad6808e8bb5f1774581ccf64aa265fef38eca80a1463d6310bb801b3ba7"), 0xFFFFFFFFFFFFL)) } }