From 3c09918435cba92b01fcae897f072b3cf7ead1cc Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 9 Aug 2022 19:33:36 +0200 Subject: [PATCH] Check block proof-of-work When a tx is confirmed, we check the proof-of-work of the block in which it was published as well as the following blocks. For each block, we check that their hash is below the difficulty they advertise, and that this difficulty is below a configured threshold. --- .../scala/fr/acinq/eclair/NodeParams.scala | 14 ++++++++--- .../blockchain/bitcoind/ZmqWatcher.scala | 23 +----------------- .../bitcoind/rpc/BitcoinCoreClient.scala | 24 ++++++++++++++++--- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +++- .../bitcoind/BitcoinCoreClientSpec.scala | 8 +++---- 5 files changed, 40 insertions(+), 33 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index a3e7791e53..a386bb9dff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair import com.typesafe.config.{Config, ConfigFactory, ConfigValueType} +import fr.acinq.bitcoin.UInt256 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Satoshi, computeP2WpkhAddress} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.ChannelFlags @@ -36,6 +37,7 @@ import fr.acinq.eclair.router.PathFindingExperimentConf import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries} import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress} +import fr.acinq.secp256k1.Hex import grizzled.slf4j.Logging import scodec.bits.ByteVector @@ -82,7 +84,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, blockchainWatchdogThreshold: Int, blockchainWatchdogSources: Seq[String], onionMessageConfig: OnionMessageConfig, - purgeInvoicesInterval: Option[FiniteDuration]) { + purgeInvoicesInterval: Option[FiniteDuration], + minBlockDifficulty_opt: Option[UInt256]) { val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey val nodeId: PublicKey = nodeKeyManager.nodeId @@ -401,6 +404,10 @@ object NodeParams extends Logging { None } + val minBlockDifficulty_opt = if (config.hasPath("minimum-block-difficulty")) + Some(new UInt256(Hex.decode(config.getString("minimum-block-difficulty")))) + else None + NodeParams( nodeKeyManager = nodeKeyManager, channelKeyManager = channelKeyManager, @@ -511,7 +518,8 @@ object NodeParams extends Logging { relayPolicy = onionMessageRelayPolicy, timeout = FiniteDuration(config.getDuration("onion-messages.reply-timeout").getSeconds, TimeUnit.SECONDS), ), - purgeInvoicesInterval = purgeInvoicesInterval + purgeInvoicesInterval = purgeInvoicesInterval, + minBlockDifficulty_opt = minBlockDifficulty_opt ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index 5e4d246183..61aa07e3da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -405,7 +405,7 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client // matter because this only happens once, when the watched transaction has reached min_depth client.getTxConfirmations(w.txId).flatMap { case Some(confirmations) if confirmations >= w.minDepth => - client.checkTxConfirmations(w.txId, confirmations).flatMap { isValid => + client.checkTxConfirmations(w.txId, confirmations, this.nodeParams.minBlockDifficulty_opt).flatMap { isValid => require(isValid, s"invalid confirmation proof for ${w.txId}") client.getTransaction(w.txId).flatMap { tx => client.getTransactionShortId(w.txId).map { @@ -421,25 +421,4 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client case _ => Future.successful((): Unit) } } - - def checkConfirmedOld(w: WatchConfirmed[_ <: WatchConfirmedTriggered]): Future[Unit] = { - log.debug("checking confirmations of txid={}", w.txId) - // NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really - // matter because this only happens once, when the watched transaction has reached min_depth - client.getTxConfirmations(w.txId).flatMap { - case Some(confirmations) if confirmations >= w.minDepth => - client.getTransaction(w.txId).flatMap { tx => - client.getTransactionShortId(w.txId).map { - case (height, index) => w match { - case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx)) - case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx)) - case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx)) - case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx)) - } - } - } - case _ => Future.successful((): Unit) - } - } - } 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 7d4288eca2..b461965794 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 @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.{Bech32, Block, BlockHeader} +import fr.acinq.bitcoin.{Bech32, Block, BlockHeader, UInt256} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} @@ -92,15 +92,33 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall }) } - def checkTxConfirmations(txid: ByteVector32, confirmations: Int)(implicit ec: ExecutionContext): Future[Boolean] = { + /** + * + * @param txid transactions id + * @param confirmations number of confirmations + * @param difficultyTarget maximum difficulty threshold + * @return true of transaction `txid` was confirmed at least `confirmations` times + */ + def checkTxConfirmations(txid: ByteVector32, confirmations: Int, difficultyTarget_opt: Option[UInt256])(implicit ec: ExecutionContext): Future[Boolean] = { import KotlinUtils._ def validate(headerInfos: List[BlockHeaderInfo]): Boolean = { + // check that headers are chained together for (i <- 0 until headerInfos.size - 1) { if(!headerInfos(i).nextBlockHash.contains(kmp2scala(headerInfos(i + 1).header.hash))) return false if (headerInfos(i + 1).header.hashPreviousBlock != headerInfos(i).header.hash) return false } - true + // and check that headers include a valid proof of work + // this checks that hash(header) is smaller than the header's difficulty + // and that the header's difficulty is smaller than the provided target + difficultyTarget_opt.foreach(difficultyTarget => { + headerInfos.forall(h => { + val decoded = UInt256.decodeCompact(h.header.bits) + // check that header difficulty is not negative, does not overflow and is below our target + !decoded.getSecond && !decoded.getThird && decoded.getFirst.compareTo(difficultyTarget) <= 0 + }) + }) + headerInfos.forall(h => BlockHeader.checkProofOfWork(h.header)) } for { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index e7366e416e..563ce6e80e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -202,6 +202,7 @@ object TestConstants { timeout = 1 minute ), purgeInvoicesInterval = None, + minBlockDifficulty_opt = None ) def channelParams: LocalParams = Peer.makeChannelParams( @@ -343,7 +344,8 @@ object TestConstants { relayPolicy = RelayAll, timeout = 1 minute ), - purgeInvoicesInterval = None + purgeInvoicesInterval = None, + minBlockDifficulty_opt = None ) def channelParams: LocalParams = Peer.makeChannelParams( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index e0fba7dd12..0e89046e5e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -862,14 +862,14 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref) sender.expectMsg(Some(0)) - bitcoinClient.checkTxConfirmations(tx1.txid, 1).pipeTo(sender.ref) + bitcoinClient.checkTxConfirmations(tx1.txid, 1, None).pipeTo(sender.ref) sender.expectMsgType[Failure] generateBlocks(3) bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref) sender.expectMsg(Some(3)) - bitcoinClient.checkTxConfirmations(tx1.txid, 3).pipeTo(sender.ref) + bitcoinClient.checkTxConfirmations(tx1.txid, 3, None).pipeTo(sender.ref) assert(sender.expectMsgType[Boolean]) } @@ -890,14 +890,14 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref) sender.expectMsg(Some(0)) - bitcoinClient.checkTxConfirmations(tx1.txid, 1).pipeTo(sender.ref) + bitcoinClient.checkTxConfirmations(tx1.txid, 1, None).pipeTo(sender.ref) sender.expectMsgType[Failure] generateBlocks(3) bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref) sender.expectMsg(Some(3)) - bitcoinClient.checkTxConfirmations(tx1.txid, 3).pipeTo(sender.ref) + bitcoinClient.checkTxConfirmations(tx1.txid, 3, None).pipeTo(sender.ref) val failure = sender.expectMsgType[Failure] assert(failure.cause.getMessage.contains(s"cannot find inclusion proof for ${tx1.txid}")) }