Skip to content

Commit 949424a

Browse files
committed
Rework and simplify tx inclusion proofs
We can use the tx position that we compute when we verify the inclusion proof instead of explicitly asking Bitcoin Core for it. We also check that the id of the tx that is returned match what we asked for, and add min-difficulty checks for regtest and testnet.
1 parent 3ec4af7 commit 949424a

File tree

4 files changed

+39
-38
lines changed

4 files changed

+39
-38
lines changed

eclair-core/src/main/resources/reference.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ eclair {
4040
// - ignore: eclair will leave these utxos locked and start
4141
startup-locked-utxos-behavior = "stop"
4242
final-pubkey-refresh-delay = 3 seconds
43-
min-difficulty = 387294044 // difficulty of block 600000
43+
min-difficulty-target = 387294044 // difficulty of block 600000
4444
}
4545

4646
node-alias = "eclair"

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

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -416,35 +416,30 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
416416
private def checkConfirmed(w: WatchConfirmed[_ <: WatchConfirmedTriggered]): Future[Unit] = {
417417
log.debug("checking confirmations of txid={}", w.txId)
418418

419-
def checkConfirmationProof(): Future[Unit] = {
420-
client.getTxConfirmationProof(w.txId).map(headerInfos => {
421-
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
422-
// 0x1715a35cL = 387294044 = difficulty of block 600000
423-
val minDiff = Try(context.system.settings.config.getLong("eclair.bitcoind.min-difficulty")).getOrElse(0x1715a35cL)
424-
require(headerInfos.forall(hi => hi.header.bits < minDiff))
425-
}
426-
})
419+
val minDifficultyTarget = nodeParams.chainHash match {
420+
case Block.LivenetGenesisBlock.hash => Try(context.system.settings.config.getLong("eclair.bitcoind.min-difficulty-target")).getOrElse(0x1715a35cL) // 0x1715a35cL = 387294044 = difficulty target of block 600000
421+
case Block.TestnetGenesisBlock.hash => 0x1d00ffffL
422+
case _ => 0x207fffffL
427423
}
428424

429-
// NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really
430-
// matter because this only happens once, when the watched transaction has reached min_depth
431425
client.getTxConfirmations(w.txId).flatMap {
432426
case Some(confirmations) if confirmations >= w.minDepth =>
433-
checkConfirmationProof().andThen(_ =>
434-
client.getTransaction(w.txId).flatMap { tx =>
435-
client.getTransactionShortId(w.txId).map {
436-
case (height, index) => w match {
437-
case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx))
438-
case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx))
439-
case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx))
440-
case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx))
441-
case w: WatchAlternativeCommitTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchAlternativeCommitTxConfirmedTriggered(height, index, tx))
442-
}
443-
}
427+
for {
428+
proof <- client.getTxConfirmationProof(w.txId)
429+
_ = require(proof.confirmations >= confirmations)
430+
_ = require(proof.headerInfos.forall(hi => hi.header.bits <= minDifficultyTarget))
431+
height = BlockHeight(proof.height)
432+
tx <- client.getTransaction(w.txId)
433+
} yield {
434+
w match {
435+
case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, proof.txIndex, tx))
436+
case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, proof.txIndex, tx))
437+
case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, proof.txIndex, tx))
438+
case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, proof.txIndex, tx))
439+
case w: WatchAlternativeCommitTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchAlternativeCommitTxConfirmedTriggered(height, proof.txIndex, tx))
444440
}
445-
)
446-
case _ => Future.successful((): Unit)
441+
}
442+
case _ => Future.successful(())
447443
}
448444
}
449-
450445
}

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
5252
//------------------------- TRANSACTIONS -------------------------//
5353

5454
def getTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] =
55-
getRawTransaction(txid).map(raw => Transaction.read(raw))
55+
getRawTransaction(txid).map(raw => {
56+
val tx = Transaction.read(raw)
57+
require(tx.txid == txid, "transaction id mismatch")
58+
tx
59+
})
5660

5761
private def getRawTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[String] =
5862
rpcClient.invoke("getrawtransaction", txid).collect {
@@ -79,7 +83,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
7983
* @param txid transaction id
8084
* @return a list of block header information, starting from the block in which the transaction was published, up to the current tip
8185
*/
82-
def getTxConfirmationProof(txid: ByteVector32)(implicit ec: ExecutionContext): Future[List[BlockHeaderInfo]] = {
86+
def getTxConfirmationProof(txid: ByteVector32)(implicit ec: ExecutionContext): Future[TxConfirmationProof] = {
8387
import KotlinUtils._
8488

8589
/**
@@ -103,16 +107,13 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
103107
// that can be used to rebuild the block's merkle root
104108
(header, txHashesAndPos) = verifyTxOutProof(proof)
105109
// inclusionData contains a header and a list of (txid, position) that can be used to re-build the header's merkle root
106-
// check that the block hash included in the proof matches the block in which the tx was published
107-
Some(blockHash) <- getTxBlockHash(txid)
108-
_ = require(header.blockId.contentEquals(blockHash.toArray), "confirmation proof is not valid (block id mismatch)")
109-
// check that our txid is included in the merkle root of the block it was published in
110-
txids = txHashesAndPos.map { case (txhash, _) => txhash.reverse }
111-
_ = require(txids.contains(txid), "confirmation proof is not valid (txid not found)")
110+
// find the position of txid in the merkle root of the block it was published in
111+
pos_opt = txHashesAndPos.find { case (hash, _) => hash.reverse == txid } map { case (_, pos) => pos }
112+
_ = require(pos_opt.isDefined, "confirmation proof is not valid (txid not found)")
112113
// get the block in which our tx was confirmed and all following blocks
113-
headerInfos <- getBlockInfos(blockHash, confirmations_opt.get)
114-
_ = require(headerInfos.head.header.blockId.contentEquals(blockHash.toArray), "block header id mismatch")
115-
} yield headerInfos
114+
headerInfos <- getBlockInfos(header.blockId, confirmations_opt.get)
115+
_ = require(headerInfos.head.header.blockId == header.blockId, "block header id mismatch")
116+
} yield TxConfirmationProof(txid, headerInfos, pos_opt.get)
116117
}
117118

118119
def getTxOutProof(txid: ByteVector32)(implicit ec: ExecutionContext): Future[ByteVector] =
@@ -717,6 +718,11 @@ object BitcoinCoreClient {
717718

718719
case class BlockHeaderInfo(header: BlockHeader, confirmation: Long, height: Long, nextBlockHash: Option[ByteVector32])
719720

721+
case class TxConfirmationProof(txid: ByteVector32, headerInfos: List[BlockHeaderInfo], txIndex: Int) {
722+
val confirmations = headerInfos.size
723+
val height = headerInfos.head.height
724+
}
725+
720726
def toSatoshi(btcAmount: BigDecimal): Satoshi = Satoshi(btcAmount.bigDecimal.scaleByPowerOfTen(8).longValue)
721727

722728
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,8 +1379,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
13791379
val check = fr.acinq.bitcoin.Block.verifyTxOutProof(proof.toArray)
13801380
val header = check.getFirst
13811381
bitcoinClient.getTxConfirmationProof(tx.txid).pipeTo(sender.ref)
1382-
val headerInfos = sender.expectMsgType[List[BlockHeaderInfo]]
1383-
assert(header == headerInfos.head.header)
1382+
val confirmationProof = sender.expectMsgType[TxConfirmationProof]
1383+
assert(header == confirmationProof.headerInfos.head.header)
13841384

13851385
// try again with a bitcoin client that returns a proof that is not valid for our tx but from the same block where it was confirmed
13861386
bitcoinClient.getTxOutProof(dummyTx.txid).pipeTo(sender.ref)

0 commit comments

Comments
 (0)