diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 11e93b9ca8..cbc574fc82 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -40,6 +40,7 @@ eclair { // - ignore: eclair will leave these utxos locked and start startup-locked-utxos-behavior = "stop" final-pubkey-refresh-delay = 3 seconds + min-difficulty-target = 387294044 // difficulty of block 600000 } node-alias = "eclair" 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 5455a06a70..86c58780ea 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 @@ -30,7 +30,7 @@ import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, RealShortChannelId, T import java.util.concurrent.atomic.AtomicLong import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} +import scala.util.{Failure, Success, Try} /** * Created by PM on 21/02/2016. @@ -415,23 +415,31 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client private def checkConfirmed(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 + + val minDifficultyTarget = nodeParams.chainHash match { + case Block.LivenetGenesisBlock.hash => Try(context.system.settings.config.getLong("eclair.bitcoind.min-difficulty-target")).getOrElse(0x1715a35cL) // 0x1715a35cL = 387294044 = difficulty target of block 600000 + case Block.TestnetGenesisBlock.hash => 0x1d00ffffL + case _ => 0x207fffffL + } + 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 w: WatchAlternativeCommitTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchAlternativeCommitTxConfirmedTriggered(height, index, tx)) - } + for { + proof <- client.getTxConfirmationProof(w.txId) + _ = require(proof.confirmations >= confirmations) + _ = require(proof.headerInfos.forall(hi => hi.header.bits <= minDifficultyTarget)) + height = BlockHeight(proof.height) + tx <- client.getTransaction(w.txId) + } yield { + w match { + case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, proof.txIndex, tx)) + case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, proof.txIndex, tx)) + case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, proof.txIndex, tx)) + case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, proof.txIndex, tx)) + case w: WatchAlternativeCommitTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchAlternativeCommitTxConfirmedTriggered(height, proof.txIndex, tx)) } } - case _ => Future.successful((): Unit) + case _ => Future.successful(()) } } - } 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 476fd8a777..b0a8f3a52c 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} +import fr.acinq.bitcoin.{Bech32, Block, BlockHeader} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse} @@ -52,7 +52,11 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall //------------------------- TRANSACTIONS -------------------------// def getTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] = - getRawTransaction(txid).map(raw => Transaction.read(raw)) + getRawTransaction(txid).map(raw => { + val tx = Transaction.read(raw) + require(tx.txid == txid, "transaction id mismatch") + tx + }) private def getRawTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[String] = rpcClient.invoke("getrawtransaction", txid).collect { @@ -66,21 +70,107 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall JInt(timestamp) = blockchainInfo \ "mediantime" } yield GetTxWithMetaResponse(txid, tx_opt, TimestampSecond(timestamp.toLong)) - /** Get the number of confirmations of a given transaction. */ - def getTxConfirmations(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[Int]] = - rpcClient.invoke("getrawtransaction", txid, 1 /* verbose output is needed to get the number of confirmations */) - .map(json => Some((json \ "confirmations").extractOrElse[Int](0))) + + def getTxInfo(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[TransactionInfo]] = + rpcClient.invoke("getrawtransaction", txid, 1).map(json => { + val confirmations = Some((json \ "confirmations").extractOrElse[Int](0)) + val blockHash = (json \ "blockhash").extractOpt[String].map(ByteVector32.fromValidHex) + val hex = (json \ "hex").extract[String] + val tx = Transaction.read(hex) + require(tx.txid == txid, "transaction id mismatch") + Some(TransactionInfo(tx, confirmations, blockHash)) + }) .recover { case t: JsonRPCError if t.error.code == -5 => None // Invalid or non-wallet transaction id (code: -5) } - /** Get the hash of the block containing a given transaction. */ - private def getTxBlockHash(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[ByteVector32]] = - rpcClient.invoke("getrawtransaction", txid, 1 /* verbose output is needed to get the block hash */) - .map(json => (json \ "blockhash").extractOpt[String].map(ByteVector32.fromValidHex)) - .recover { - case t: JsonRPCError if t.error.code == -5 => None // Invalid or non-wallet transaction id (code: -5) + def getTxConfirmations(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[Int]] = getTxInfo(txid).map(_.flatMap(_.confirmations)) + + /** + * + * @param txid transaction id + * @return a list of block header information, starting from the block in which the transaction was published, up to the current tip + */ + def getTxConfirmationProof(txid: ByteVector32)(implicit ec: ExecutionContext): Future[TxConfirmationProof] = { + import KotlinUtils._ + + /** + * Scala wrapper for Block.verifyTxOutProof + * + * @param proof tx output proof, as provided by bitcoind + * @return a (Header, List(txhash, position)) tuple. Header is the header of the block used to compute the input proof, and + * (txhash, position) is a list of transaction ids that were verified, and their position in the block + */ + def verifyTxOutProof(proof: ByteVector): (BlockHeader, List[(ByteVector32, Int)]) = { + val check = Block.verifyTxOutProof(proof.toArray) + (check.getFirst, check.getSecond.asScala.toList.map(p => (kmp2scala(p.getFirst), p.getSecond.intValue()))) + } + + for { + txinfo <- getTxInfo(txid) + confirmations_opt = txinfo.flatMap(_.confirmations) + if confirmations_opt.isDefined && confirmations_opt.get > 0 + blockId = txinfo.flatMap(_.blockHash).get + // get the coinbase tx for this block, we'll use it to check the block's height + blockTxids <- getTransactionIds(blockId) + coinbaseTxid = blockTxids.head + coinbaseTx <- getTransaction(coinbaseTxid) + // get the merkle proof for our txid + proof <- getTxOutProof(Seq(txid, coinbaseTxid), Some(blockId)) + // verify this merkle proof. if valid, we get the header for the block the tx was published in, and the tx hashes + // that can be used to rebuild the block's merkle root + (header, txHashesAndPos) = verifyTxOutProof(proof) + _ = require(txHashesAndPos.exists { case (hash, _) => hash.reverse == coinbaseTxid }, "confirmation proof is not valid for coinbase tx") + // inclusionData contains a header and a list of (txid, position) that can be used to re-build the header's merkle root + // find the position of txid in the merkle root of the block it was published in + pos_opt = txHashesAndPos.find { case (hash, _) => hash.reverse == txid } map { case (_, pos) => pos } + _ = require(pos_opt.isDefined, "confirmation proof is not valid (txid not found)") + // get the block in which our tx was confirmed and all following blocks + height = decodeBlockHeight(coinbaseTx) + headerInfos <- getBlockInfos(header.blockId, confirmations_opt.get) + _ = require(headerInfos.head.header.blockId == header.blockId, "block header id mismatch") + _ = require(headerInfos.head.height == height, "block header height mismatch") + } yield TxConfirmationProof(txid, headerInfos, pos_opt.get) + } + + def getTxOutProof(txids: Seq[ByteVector32], blockHash_opt: Option[ByteVector32])(implicit ec: ExecutionContext): Future[ByteVector] = (blockHash_opt match { + case Some(blockHash) => rpcClient.invoke("gettxoutproof", txids.toArray, blockHash) + case None => rpcClient.invoke("gettxoutproof", txids.toArray) + }).collect { case JString(raw) => ByteVector.fromValidHex(raw) } + + def getTxOutProof(txid: ByteVector32, blockHash_opt: Option[ByteVector32] = None)(implicit ec: ExecutionContext): Future[ByteVector] = getTxOutProof(Seq(txid), blockHash_opt) + + def getBlock(blockId: ByteVector32)(implicit ec: ExecutionContext): Future[Block] = { + rpcClient.invoke("getblock", blockId.toString(), 0).collect { + case JString(raw) => + val block = Block.read(raw) + require(block.blockId.contentEquals(blockId.toArray)) + block + } + } + + // returns a chain a blocks of a given size starting at `blockId` + def getBlockInfos(blockId: ByteVector32, count: Int)(implicit ec: ExecutionContext): Future[List[BlockHeaderInfo]] = { + import KotlinUtils._ + + def loop(blocks: List[BlockHeaderInfo]): Future[List[BlockHeaderInfo]] = if (blocks.size == count) Future.successful(blocks) else { + getBlockHeaderInfo(blocks.last.nextBlockId.get).flatMap(info => loop(blocks :+ info)) + } + + getBlockHeaderInfo(blockId).flatMap(info => loop(List(info))).map(blocks => { + for (i <- 0 until blocks.size - 1) { + require(BlockHeader.checkProofOfWork(blocks(i).header)) + require(blocks(i).height == blocks(0).height + i) + require(blocks(i).confirmation == blocks(0).confirmation - i) + require(blocks(i).nextBlockId.contains(kmp2scala(blocks(i + 1).header.blockId)), "next block id mismatch") + require(blocks(i + 1).header.hashPreviousBlock == blocks(i).header.hash, "previous block id mismatch") } + blocks + }) + } + + /** Get the hash of the block containing a given transaction. */ + private def getTxBlockHash(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[ByteVector32]] = getTxInfo(txid).map(i => i.flatMap(_.blockHash)) /** * @return a Future[height, index] where height is the height of the block where this transaction was published, and @@ -207,6 +297,39 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall case _ => Nil } + //------------------------- BLOCKS -------------------------// + def getBlockHash(height: Int)(implicit ec: ExecutionContext): Future[ByteVector32] = { + rpcClient.invoke("getblockhash", height).map(json => { + val JString(hash) = json + ByteVector32.fromValidHex(hash) + }) + } + + def getBlockHeaderInfo(blockId: ByteVector32)(implicit ec: ExecutionContext): Future[BlockHeaderInfo] = { + import fr.acinq.bitcoin.{ByteVector32 => ByteVector32Kt} + rpcClient.invoke("getblockheader", blockId.toString()).map(json => { + val JInt(confirmations) = json \ "confirmations" + val JInt(height) = json \ "height" + val JInt(time) = json \ "time" + val JInt(version) = json \ "version" + val JInt(nonce) = json \ "nonce" + val JString(bits) = json \ "bits" + val merkleRoot = ByteVector32Kt.fromValidHex((json \ "merkleroot").extract[String]).reversed() + val previousblockId = ByteVector32Kt.fromValidHex((json \ "previousblockhash").extract[String]) + val nextblockId = (json \ "nextblockhash").extractOpt[String].map(h => ByteVector32.fromValidHex(h)) + val header = new BlockHeader(version.longValue, previousblockId.reversed(), merkleRoot, time.longValue, java.lang.Long.parseLong(bits, 16), nonce.longValue) + require(header.blockId == KotlinUtils.scala2kmp(blockId)) + BlockHeaderInfo(header, confirmations.toLong, height.toLong, nextblockId) + }) + } + + def getTransactionIds(blockId: ByteVector32)(implicit ec: ExecutionContext): Future[Seq[ByteVector32]] = { + rpcClient.invoke("getblock", blockId.toString(), 1).map(json => { + val JArray(txids) = json \ "tx" + txids.map(txid => ByteVector32.fromValidHex(txid.extract[String])) + }) + } + //------------------------- FUNDING -------------------------// def fundTransaction(tx: Transaction, options: FundTransactionOptions)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { @@ -623,6 +746,35 @@ object BitcoinCoreClient { case class Utxo(txid: ByteVector32, amount: MilliBtc, confirmations: Long, safe: Boolean, label_opt: Option[String]) + case class TransactionInfo(tx: Transaction, confirmations: Option[Int], blockHash: Option[ByteVector32]) + + case class BlockHeaderInfo(header: BlockHeader, confirmation: Long, height: Long, nextBlockId: Option[ByteVector32]) + + case class BlockInfo(headerInfo: BlockHeaderInfo, txids: Seq[ByteVector32]) + + case class TxConfirmationProof(txid: ByteVector32, headerInfos: List[BlockHeaderInfo], txIndex: Int) { + val confirmations = headerInfos.size + val height = headerInfos.head.height + } + def toSatoshi(btcAmount: BigDecimal): Satoshi = Satoshi(btcAmount.bigDecimal.scaleByPowerOfTen(8).longValue) + /** + * TODO: move this into bitcoin-kmp + * Extract the block height embedded in a block's coinbase tx (see BIP34) + * + * @param signatureScript coinbase transaction input signature script + * @return the block height committed to in the coinbase tx + */ + def decodeBlockHeight(signatureScript: ByteVector): Long = { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + + val it = fr.acinq.bitcoin.Script.INSTANCE.scriptIterator(signatureScript.toArray) + val op: ScriptElt = it.next() + require(op.isPush, "signature script does not match BIP34 rules") + Script.decodeNumber(op.asInstanceOf[OP_PUSHDATA].data, false, 4) + } + + def decodeBlockHeight(coinbaseTx: Transaction): Long = decodeBlockHeight(coinbaseTx.txIn(0).signatureScript) + } \ No newline at end of file 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 ed7e8b526d..0d2cc7ada8 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 @@ -21,14 +21,14 @@ import akka.pattern.pipe import akka.testkit.TestProbe import fr.acinq.bitcoin import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, OP_DROP, OP_PUSHDATA, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, computeP2PkhAddress, computeP2WpkhAddress} -import fr.acinq.bitcoin.{Bech32, SigHash, SigVersion} +import fr.acinq.bitcoin.scalacompat.{Btc, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, OP_DROP, OP_PUSHDATA, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, computeP2PkhAddress, computeP2WpkhAddress} +import fr.acinq.bitcoin.{Bech32, Block, SigHash, SigVersion} import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse} import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient, BitcoinJsonRPCClient, JsonRPCError} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, addressToPublicKeyScript, randomBytes32, randomKey} @@ -308,6 +308,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } test("create/commit/rollback funding txs") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + val sender = TestProbe() val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) @@ -372,6 +374,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } test("unlock failed funding txs") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + val sender = TestProbe() val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) @@ -1224,6 +1228,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } test("get pubkey for p2wpkh receive address") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val sender = TestProbe() val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) @@ -1247,6 +1252,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } test("generate segwit change outputs") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val sender = TestProbe() val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) @@ -1321,4 +1327,102 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A assert(sender.expectMsgType[Transaction].txid == tx.txid) } + test("get block header info") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + val sender = TestProbe() + val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) + bitcoinClient.getBlockHeight().pipeTo(sender.ref) + val height = sender.expectMsgType[BlockHeight] + bitcoinClient.getBlockHash(height.toInt).pipeTo(sender.ref) + val lastBlockId = sender.expectMsgType[ByteVector32] + bitcoinClient.getBlockHeaderInfo(lastBlockId).pipeTo(sender.ref) + val lastBlockInfo = sender.expectMsgType[BlockHeaderInfo] + assert(lastBlockInfo.nextBlockId.isEmpty) + + bitcoinClient.getBlockHash(height.toInt - 1).pipeTo(sender.ref) + val blockId = sender.expectMsgType[ByteVector32] + bitcoinClient.getBlockHeaderInfo(blockId).pipeTo(sender.ref) + val blockInfo = sender.expectMsgType[BlockHeaderInfo] + assert(lastBlockInfo.header.hashPreviousBlock == blockInfo.header.hash) + assert(blockInfo.nextBlockId.contains(kmp2scala(lastBlockInfo.header.blockId))) + } + + test("get chains of block headers") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + val sender = TestProbe() + val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) + + bitcoinClient.getBlockHash(140).pipeTo(sender.ref) + val blockId = sender.expectMsgType[ByteVector32] + bitcoinClient.getBlockInfos(blockId, 5).pipeTo(sender.ref) + val blockInfos = sender.expectMsgType[List[BlockHeaderInfo]] + for (i <- 0 until blockInfos.size - 1) { + require(blockInfos(i).nextBlockId.contains(kmp2scala(blockInfos(i + 1).header.blockId))) + require(blockInfos(i + 1).header.hashPreviousBlock == blockInfos(i).header.hash) + } + } + + test("get transaction ids") { + val sender = TestProbe() + val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) + val address = getNewAddress(sender) + + generateBlocks(1) + val txs = (0 until 10).map(_ => sendToAddress(address, 10000.sat)) + generateBlocks(1) + bitcoinClient.getBlockHeight().flatMap(height => bitcoinClient.getBlockHash(height.toInt)).pipeTo(sender.ref) + val blockHash = sender.expectMsgType[ByteVector32] + bitcoinClient.getBlock(blockHash).pipeTo(sender.ref) + val block = sender.expectMsgType[Block] + assert(block.blockId.contentEquals(blockHash.toArray)) + bitcoinClient.getTransactionIds(blockHash).pipeTo(sender.ref) + val txids = sender.expectMsgType[Seq[ByteVector32]] + assert((txs.map(_.txid).toSet -- txids).isEmpty) + } + + test("decode embedded block height") { + val coinbaseTx = Transaction.read("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff580399de0a1b4d696e656420627920416e74506f6f6c373436a10015037d15afd8fabe6d6d84ec36731b79c697ddf94ce60c369686671fddcfa1d89bab89445908e0acf31f0200000000000000514900009520020000000000ffffffff04d0ee6425000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac0000000000000000266a24aa21a9ed271daeb7b480018dd1dc9c53180c1a7eb89d004e7eb4ec2937e69909f3782a030000000000000000266a24b9e11b6d68d1fd4e697128e0b3901d0229ae7cd7f14124d9678aae7cb716beebe65fb1b800000000000000002b6a2952534b424c4f434b3af5be02402ea85e79a5198ef3a6988aded3fa928a6609120f27905328003b6f7b0120000000000000000000000000000000000000000000000000000000000000000000000000") + require(decodeBlockHeight(coinbaseTx) == 712345) + } + + test("verify tx publication proofs") { + val sender = TestProbe() + val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) + val address = getNewAddress(sender) + + // we create a dummy confirmed tx, we'll use its txout proof later + val dummyTx = sendToAddress(address, 5 btc, sender) + + val tx = sendToAddress(address, 5 btc, sender) + // transaction is not confirmed yet + bitcoinClient.getTxConfirmations(tx.txid).pipeTo(sender.ref) + sender.expectMsg(Some(0)) + + // let's confirm our transaction. + generateBlocks(6) + bitcoinClient.getTxConfirmations(tx.txid).pipeTo(sender.ref) + sender.expectMsg(Some(6)) + + bitcoinClient.getBlockHeight().flatMap(height => bitcoinClient.getBlockHash(height.toInt - 5)).pipeTo(sender.ref) + val blockHash = sender.expectMsgType[ByteVector32] + bitcoinClient.getTxOutProof(tx.txid, Some(blockHash)).pipeTo(sender.ref) + val proof = sender.expectMsgType[ByteVector] + val check = fr.acinq.bitcoin.Block.verifyTxOutProof(proof.toArray) + val header = check.getFirst + bitcoinClient.getTxConfirmationProof(tx.txid).pipeTo(sender.ref) + val confirmationProof = sender.expectMsgType[TxConfirmationProof] + assert(header == confirmationProof.headerInfos.head.header) + + // 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 + bitcoinClient.getTxOutProof(dummyTx.txid).pipeTo(sender.ref) + val dumyProof = sender.expectMsgType[ByteVector] + val evilBitcoinClient = new BitcoinCoreClient(new BitcoinJsonRPCClient { + override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] = method match { + case "gettxoutproof" => Future.successful(JString(dumyProof.toHex)) + case _ => bitcoinrpcclient.invoke(method, params: _*)(ec) + } + }) + evilBitcoinClient.getTxConfirmationProof(tx.txid).pipeTo(sender.ref) + sender.expectMsgType[Failure] + } } \ No newline at end of file