Skip to content

Commit a13d7af

Browse files
committed
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.
1 parent 3657dfa commit a13d7af

File tree

5 files changed

+57
-34
lines changed

5 files changed

+57
-34
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
package fr.acinq.eclair
1818

1919
import com.typesafe.config.{Config, ConfigFactory, ConfigValueType}
20+
import fr.acinq.bitcoin.UInt256
2021
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
21-
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Satoshi}
22+
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Satoshi, computeP2WpkhAddress}
2223
import fr.acinq.eclair.Setup.Seeds
2324
import fr.acinq.eclair.blockchain.fee._
2425
import fr.acinq.eclair.channel.ChannelFlags
@@ -36,6 +37,7 @@ import fr.acinq.eclair.router.PathFindingExperimentConf
3637
import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries}
3738
import fr.acinq.eclair.tor.Socks5ProxyParams
3839
import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress}
40+
import fr.acinq.secp256k1.Hex
3941
import grizzled.slf4j.Logging
4042
import scodec.bits.ByteVector
4143

@@ -82,7 +84,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
8284
blockchainWatchdogThreshold: Int,
8385
blockchainWatchdogSources: Seq[String],
8486
onionMessageConfig: OnionMessageConfig,
85-
purgeInvoicesInterval: Option[FiniteDuration]) {
87+
purgeInvoicesInterval: Option[FiniteDuration],
88+
minBlockDifficulty_opt: Option[UInt256]) {
8689
val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey
8790

8891
val nodeId: PublicKey = nodeKeyManager.nodeId
@@ -401,6 +404,10 @@ object NodeParams extends Logging {
401404
None
402405
}
403406

407+
val minBlockDifficulty_opt = if (config.hasPath("minimum-block-difficulty"))
408+
Some(new UInt256(Hex.decode(config.getString("minimum-block-difficulty"))))
409+
else None
410+
404411
NodeParams(
405412
nodeKeyManager = nodeKeyManager,
406413
channelKeyManager = channelKeyManager,
@@ -511,7 +518,8 @@ object NodeParams extends Logging {
511518
relayPolicy = onionMessageRelayPolicy,
512519
timeout = FiniteDuration(config.getDuration("onion-messages.reply-timeout").getSeconds, TimeUnit.SECONDS),
513520
),
514-
purgeInvoicesInterval = purgeInvoicesInterval
521+
purgeInvoicesInterval = purgeInvoicesInterval,
522+
minBlockDifficulty_opt = minBlockDifficulty_opt
515523
)
516524
}
517525
}

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
405405
// matter because this only happens once, when the watched transaction has reached min_depth
406406
client.getTxConfirmations(w.txId).flatMap {
407407
case Some(confirmations) if confirmations >= w.minDepth =>
408-
client.checkTxConfirmations(w.txId, confirmations).flatMap { isValid =>
408+
client.checkTxConfirmations(w.txId, confirmations, this.nodeParams.minBlockDifficulty_opt).flatMap { isValid =>
409409
require(isValid, s"invalid confirmation proof for ${w.txId}")
410410
client.getTransaction(w.txId).flatMap { tx =>
411411
client.getTransactionShortId(w.txId).map {
@@ -421,25 +421,4 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
421421
case _ => Future.successful((): Unit)
422422
}
423423
}
424-
425-
def checkConfirmedOld(w: WatchConfirmed[_ <: WatchConfirmedTriggered]): Future[Unit] = {
426-
log.debug("checking confirmations of txid={}", w.txId)
427-
// NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really
428-
// matter because this only happens once, when the watched transaction has reached min_depth
429-
client.getTxConfirmations(w.txId).flatMap {
430-
case Some(confirmations) if confirmations >= w.minDepth =>
431-
client.getTransaction(w.txId).flatMap { tx =>
432-
client.getTransactionShortId(w.txId).map {
433-
case (height, index) => w match {
434-
case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx))
435-
case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx))
436-
case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx))
437-
case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx))
438-
}
439-
}
440-
}
441-
case _ => Future.successful((): Unit)
442-
}
443-
}
444-
445424
}

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc
1818

1919
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2020
import fr.acinq.bitcoin.scalacompat._
21-
import fr.acinq.bitcoin.{Bech32, Block, BlockHeader}
21+
import fr.acinq.bitcoin.{Bech32, Block, BlockHeader, UInt256}
2222
import fr.acinq.eclair.ShortChannelId.coordinates
2323
import fr.acinq.eclair.blockchain.OnChainWallet
2424
import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance}
@@ -92,15 +92,33 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
9292
})
9393
}
9494

95-
def checkTxConfirmations(txid: ByteVector32, confirmations: Int)(implicit ec: ExecutionContext): Future[Boolean] = {
95+
/**
96+
*
97+
* @param txid transactions id
98+
* @param confirmations number of confirmations
99+
* @param difficultyTarget maximum difficulty threshold
100+
* @return true of transaction `txid` was confirmed at least `confirmations` times
101+
*/
102+
def checkTxConfirmations(txid: ByteVector32, confirmations: Int, difficultyTarget_opt: Option[UInt256])(implicit ec: ExecutionContext): Future[Boolean] = {
96103
import KotlinUtils._
97104

98105
def validate(headerInfos: List[BlockHeaderInfo]): Boolean = {
106+
// check that headers are chained together
99107
for (i <- 0 until headerInfos.size - 1) {
100108
if(!headerInfos(i).nextBlockHash.contains(kmp2scala(headerInfos(i + 1).header.hash))) return false
101109
if (headerInfos(i + 1).header.hashPreviousBlock != headerInfos(i).header.hash) return false
102110
}
103-
true
111+
// and check that headers include a valid proof of work
112+
// this checks that hash(header) is smaller than the header's difficulty
113+
// and that the header's difficulty is smaller than the provided target
114+
val diffCheck = difficultyTarget_opt.forall(difficultyTarget => {
115+
headerInfos.forall(h => {
116+
val decoded = UInt256.decodeCompact(h.header.bits)
117+
// check that header difficulty is not negative, does not overflow and is below our target
118+
!decoded.getSecond && !decoded.getThird && decoded.getFirst.compareTo(difficultyTarget) <= 0
119+
})
120+
})
121+
diffCheck && headerInfos.forall(h => BlockHeader.checkProofOfWork(h.header))
104122
}
105123

106124
for {

eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ object TestConstants {
202202
timeout = 1 minute
203203
),
204204
purgeInvoicesInterval = None,
205+
minBlockDifficulty_opt = None
205206
)
206207

207208
def channelParams: LocalParams = Peer.makeChannelParams(
@@ -343,7 +344,8 @@ object TestConstants {
343344
relayPolicy = RelayAll,
344345
timeout = 1 minute
345346
),
346-
purgeInvoicesInterval = None
347+
purgeInvoicesInterval = None,
348+
minBlockDifficulty_opt = None
347349
)
348350

349351
def channelParams: LocalParams = Peer.makeChannelParams(

eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import akka.actor.Status.Failure
2121
import akka.pattern.pipe
2222
import akka.testkit.{TestActor, TestProbe}
2323
import fr.acinq.bitcoin
24-
import fr.acinq.bitcoin.BlockHeader
24+
import fr.acinq.bitcoin.{BlockHeader, UInt256}
2525
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2626
import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, MilliBtcDouble, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut}
2727
import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance}
@@ -862,15 +862,31 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
862862
bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref)
863863
sender.expectMsg(Some(0))
864864

865-
bitcoinClient.checkTxConfirmations(tx1.txid, 1).pipeTo(sender.ref)
865+
bitcoinClient.checkTxConfirmations(tx1.txid, 1, None).pipeTo(sender.ref)
866866
sender.expectMsgType[Failure]
867867

868868
generateBlocks(3)
869869
bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref)
870870
sender.expectMsg(Some(3))
871871

872-
bitcoinClient.checkTxConfirmations(tx1.txid, 3).pipeTo(sender.ref)
872+
bitcoinClient.checkTxConfirmations(tx1.txid, 3, None).pipeTo(sender.ref)
873873
assert(sender.expectMsgType[Boolean])
874+
875+
val f = for {
876+
height <- bitcoinClient.getBlockHeight()
877+
blockId <- bitcoinClient.getBlockHash(height.toInt)
878+
info <- bitcoinClient.getBlockHeaderInfo(blockId)
879+
} yield info.header
880+
f.pipeTo(sender.ref)
881+
val header = sender.expectMsgType[BlockHeader]
882+
883+
val target = UInt256.decodeCompact(header.bits).getFirst
884+
bitcoinClient.checkTxConfirmations(tx1.txid, 3, Some(target)).pipeTo(sender.ref)
885+
assert(sender.expectMsgType[Boolean])
886+
887+
// same difficulty check with target/2
888+
bitcoinClient.checkTxConfirmations(tx1.txid, 3, Some(target.shr(1))).pipeTo(sender.ref)
889+
assert(!sender.expectMsgType[Boolean])
874890
}
875891

876892
test("verify tx confirmation proof") {
@@ -890,14 +906,14 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
890906
bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref)
891907
sender.expectMsg(Some(0))
892908

893-
bitcoinClient.checkTxConfirmations(tx1.txid, 1).pipeTo(sender.ref)
909+
bitcoinClient.checkTxConfirmations(tx1.txid, 1, None).pipeTo(sender.ref)
894910
sender.expectMsgType[Failure]
895911

896912
generateBlocks(3)
897913
bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref)
898914
sender.expectMsg(Some(3))
899915

900-
bitcoinClient.checkTxConfirmations(tx1.txid, 3).pipeTo(sender.ref)
916+
bitcoinClient.checkTxConfirmations(tx1.txid, 3, None).pipeTo(sender.ref)
901917
val failure = sender.expectMsgType[Failure]
902918
assert(failure.cause.getMessage.contains(s"cannot find inclusion proof for ${tx1.txid}"))
903919
}

0 commit comments

Comments
 (0)