From 9b1933efe42844d1a6d26ccb5fed4f150c28d665 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 11 Apr 2023 16:11:58 +0200 Subject: [PATCH] Validate addresses and keys generated by bitcoin core When eclair manages private keys, make sure that we can re-compute addresses and keys generated by bitcoin core. --- .../bitcoind/rpc/BitcoinCoreClient.scala | 38 +++++++++++++------ .../keymanager/LocalOnchainKeyManager.scala | 15 ++++++-- .../crypto/keymanager/OnchainKeyManager.scala | 9 +++++ 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index ab4cb503f6..3ea0333f15 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc import fr.acinq.bitcoin.psbt.{Psbt, UpdateFailure} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.{Bech32, Block, SigHash} +import fr.acinq.bitcoin.{Block, SigHash} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance} @@ -436,26 +436,42 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onchainKeyManag OnChainBalance(toSatoshi(confirmed), toSatoshi(unconfirmed)) }) + private def extractPublicKey(address: String)(implicit ec: ExecutionContext): Future[PublicKey] = { + for { + addressInfo <- rpcClient.invoke("getaddressinfo", address) + JString(keyPath) = addressInfo \ "hdkeypath" + JString(rawKey) = addressInfo \ "pubkey" + } yield { + val extracted = PublicKey(ByteVector.fromValidHex(rawKey)) + // check that when we manage private keys we can re-compute the public key we got from bitcoin core + // and that the address and public key match + val computed_opt = this.onchainKeyManager_opt.map(_.getPublicKey(DeterministicWallet.KeyPath(keyPath))) + require(computed_opt.forall(_ == (extracted, address)), "cannot recompute pubkey generated by bitcoin core") + extracted + } + } + def getReceiveAddress(label: String)(implicit ec: ExecutionContext): Future[String] = for { JString(address) <- rpcClient.invoke("getnewaddress", label) + _ <- extractPublicKey(address) } yield address def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = for { - address <- rpcClient.invoke("getnewaddress", "", "bech32") - JString(rawKey) <- rpcClient.invoke("getaddressinfo", address).map(_ \ "pubkey") - } yield PublicKey(ByteVector.fromValidHex(rawKey)) + JString(address) <- rpcClient.invoke("getnewaddress", "", "bech32") + pubKey <- extractPublicKey(address) + } yield pubKey /** * @return the public key hash of a bech32 raw change address. */ - def getChangeAddress()(implicit ec: ExecutionContext): Future[ByteVector] = { - rpcClient.invoke("getrawchangeaddress", "bech32").collect { - case JString(changeAddress) => - val pubkeyHash = ByteVector.view(Bech32.decodeWitnessAddress(changeAddress).getThird) - pubkeyHash - } + def getChangeAddress()(implicit ec: ExecutionContext): Future[ByteVector] = for { + JString(changeAddress) <- rpcClient.invoke("getrawchangeaddress", "bech32") + pubKey <- extractPublicKey(changeAddress) + } yield { + pubKey.hash160 } + /** * Asks Bitcoin Core to fund and broadcsat a tx that sends funds to a given pubkey script * If the current wallet uses Eclair to sign transaction, then we'll use our onchain key manager to sign the transaction, @@ -463,7 +479,6 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onchainKeyManag * - all inputs belong to us * - all outputs except for the one that sends to `pubkeyScript` belong to us * - * * @param pubkeyScript public key script to sent funds to * @param amount amount to send * @param feeratePerKw fee rate @@ -615,6 +630,7 @@ object BitcoinCoreClient { } case class ProcessPsbtResponse(psbt: Psbt, complete: Boolean) { + import KotlinUtils._ // Extract a fully signed transaction from `psbt` diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala index 7a7e5f172a..3c0c5e2371 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.ScriptWitness import fr.acinq.bitcoin.psbt.{Psbt, SignPsbtResult} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, DeterministicWallet, MnemonicCode} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, DeterministicWallet, MnemonicCode, computeBIP84Address} import fr.acinq.bitcoin.utils.EitherKt import grizzled.slf4j.Logging import scodec.bits.ByteVector @@ -80,6 +80,16 @@ class LocalOnchainKeyManager(entropy: ByteVector, chainHash: ByteVector32, passp ourInputs.foldLeft(psbt) { (p, i) => sigbnPsbtInput(p, i) } } + + override def getPublicKey(keyPath: KeyPath): (Crypto.PublicKey, String) = { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + val pub = getPrivateKey(keyPath.keyPath).publicKey() + val address = computeBIP84Address(pub, chainHash) + (pub, address) + } + + private def getPrivateKey(keyPath: fr.acinq.bitcoin.KeyPath) = fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey(master.priv, keyPath).getPrivateKey + // check that an output belongs to us i.e. we can recompute its public from its bip32 path private def isOurOutput(psbt: Psbt, outputIndex: Int) = { val output = psbt.getOutputs.get(outputIndex) @@ -87,8 +97,7 @@ class LocalOnchainKeyManager(entropy: ByteVector, chainHash: ByteVector32, passp output.getDerivationPaths.size() match { case 1 => output.getDerivationPaths.asScala.foreach { case (pub, keypath) => - val priv = fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey(master.priv, keypath.getKeyPath).getPrivateKey - val check = priv.publicKey() + val check = getPrivateKey(keypath.getKeyPath).publicKey() require(pub == check, s"cannot compute public key for $txout") require(txout.publicKeyScript.contentEquals(fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wpkh(pub))), s"output pubkeyscript does not match ours for $txout") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala index 62c62d7ae7..22803ec280 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala @@ -1,6 +1,8 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.psbt.Psbt +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath trait OnchainKeyManager { /** @@ -10,6 +12,13 @@ trait OnchainKeyManager { */ def getOnchainMasterPubKey(account: Long): String + /** + * + * @param keyPath BIP path + * @return the (public key, address) pair for this BIP32 path + */ + def getPublicKey(keyPath: KeyPath): (PublicKey, String) + /** * * @param account account number