Skip to content

Commit

Permalink
Validate addresses and keys generated by bitcoin core
Browse files Browse the repository at this point in the history
When eclair manages private keys, make sure that we can re-compute addresses and keys
generated by bitcoin core.
  • Loading branch information
sstone committed Apr 25, 2023
1 parent 205e8a3 commit c7bfaf4
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -430,34 +430,49 @@ 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,
* with the following assumptions:
* - 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
Expand Down Expand Up @@ -609,6 +624,7 @@ object BitcoinCoreClient {
}

case class ProcessPsbtResponse(psbt: Psbt, complete: Boolean) {

import KotlinUtils._

// Extract a fully signed transaction from `psbt`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,15 +80,24 @@ 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)
val txout = psbt.getGlobal.getTx.txOut.get(outputIndex)
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")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -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
Expand Down

0 comments on commit c7bfaf4

Please sign in to comment.