Skip to content

Commit

Permalink
Check block proof-of-work
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sstone committed Aug 10, 2022
1 parent 3657dfa commit 3c09918
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 33 deletions.
14 changes: 11 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ object TestConstants {
timeout = 1 minute
),
purgeInvoicesInterval = None,
minBlockDifficulty_opt = None
)

def channelParams: LocalParams = Peer.makeChannelParams(
Expand Down Expand Up @@ -343,7 +344,8 @@ object TestConstants {
relayPolicy = RelayAll,
timeout = 1 minute
),
purgeInvoicesInterval = None
purgeInvoicesInterval = None,
minBlockDifficulty_opt = None
)

def channelParams: LocalParams = Peer.makeChannelParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}

Expand All @@ -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}"))
}
Expand Down

0 comments on commit 3c09918

Please sign in to comment.