@@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc
1818
1919import fr .acinq .bitcoin .scalacompat .Crypto .PublicKey
2020import fr .acinq .bitcoin .scalacompat ._
21- import fr .acinq .bitcoin .{Bech32 , Block }
21+ import fr .acinq .bitcoin .{Bech32 , Block , BlockHeader }
2222import fr .acinq .eclair .ShortChannelId .coordinates
2323import fr .acinq .eclair .blockchain .OnChainWallet
2424import fr .acinq .eclair .blockchain .OnChainWallet .{FundTransactionResponse , MakeFundingTxResponse , OnChainBalance , SignTransactionResponse }
@@ -74,6 +74,41 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
7474 case t : JsonRPCError if t.error.code == - 5 => None // Invalid or non-wallet transaction id (code: -5)
7575 }
7676
77+ def getTxConfirmationProof (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [List [BlockHeaderInfo ]] = {
78+ import KotlinUtils ._
79+ for {
80+ confirmations_opt <- getTxConfirmations(txid)
81+ if (confirmations_opt.isDefined && confirmations_opt.get > 0 )
82+ // get the merkle proof for our txid
83+ proof <- getTxOutProof(txid)
84+ // verify this merkle proof. if valid, we get the header for the block the tx was published in, and the tx hashes
85+ // that can be used to rebuild the block's merkle root
86+ check = Block .verifyTxOutProof(proof.toArray)
87+ // check that the block hash included in the proof matches the block in which the tx was published
88+ Some (blockHash) <- getTxBlockHash(txid)
89+ _ = require(check.getFirst.blockId.contentEquals(blockHash.toArray), " confirmation proof is not valid (block id mismatch)" )
90+ // check that our txid is included in the merkle root of the block it was published in
91+ txids = check.getSecond.asScala.map(_.getFirst).map(kmp2scala).map(_.reverse)
92+ _ = require(txids.contains(txid))
93+ // get the block in which our tx was confirmed and all following blocks
94+ headerInfos <- getBlockInfos(blockHash, confirmations_opt.get)
95+ } yield headerInfos
96+ }
97+
98+ def getTxOutProof (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [ByteVector ] =
99+ rpcClient.invoke(" gettxoutproof" , Array (txid)).collect { case JString (raw) => ByteVector .fromValidHex(raw) }
100+
101+ // returns a chain a blocks of a given size starting at `blockId`
102+ def getBlockInfos (blockId : ByteVector32 , count : Int )(implicit ec : ExecutionContext ): Future [List [BlockHeaderInfo ]] = {
103+ import KotlinUtils ._
104+
105+ def loop (blocks : List [BlockHeaderInfo ]): Future [List [BlockHeaderInfo ]] = if (blocks.size == count) Future .successful(blocks) else {
106+ getBlockHeaderInfo(blocks.last.nextBlockHash.get.reverse).flatMap(info => loop(blocks :+ info))
107+ }
108+
109+ getBlockHeaderInfo(blockId).flatMap(info => loop(List (info)))
110+ }
111+
77112 /** Get the hash of the block containing a given transaction. */
78113 private def getTxBlockHash (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [Option [ByteVector32 ]] =
79114 rpcClient.invoke(" getrawtransaction" , txid, 1 /* verbose output is needed to get the block hash */ )
@@ -207,6 +242,32 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
207242 case _ => Nil
208243 }
209244
245+ // ------------------------- BLOCKS -------------------------//
246+ def getBlockHash (height : Int )(implicit ec : ExecutionContext ): Future [ByteVector32 ] = {
247+ rpcClient.invoke(" getblockhash" , height).map(json => {
248+ val JString (hash) = json
249+ ByteVector32 .fromValidHex(hash)
250+ })
251+ }
252+
253+ def getBlockHeaderInfo (blockId : ByteVector32 )(implicit ec : ExecutionContext ): Future [BlockHeaderInfo ] = {
254+ import fr .acinq .bitcoin .{ByteVector32 => ByteVector32Kt }
255+ rpcClient.invoke(" getblockheader" , blockId.toString()).map(json => {
256+ val JInt (confirmations) = json \ " confirmations"
257+ val JInt (height) = json \ " height"
258+ val JInt (time) = json \ " time"
259+ val JInt (version) = json \ " version"
260+ val JInt (nonce) = json \ " nonce"
261+ val JString (bits) = json \ " bits"
262+ val merkleRoot = ByteVector32Kt .fromValidHex((json \ " merkleroot" ).extract[String ]).reversed()
263+ val previousblockhash = ByteVector32Kt .fromValidHex((json \ " previousblockhash" ).extract[String ]).reversed()
264+ val nextblockhash = (json \ " nextblockhash" ).extractOpt[String ].map(h => ByteVector32 .fromValidHex(h).reverse)
265+ val header = new BlockHeader (version.longValue, previousblockhash, merkleRoot, time.longValue, java.lang.Long .parseLong(bits, 16 ), nonce.longValue)
266+ require(header.blockId == KotlinUtils .scala2kmp(blockId))
267+ BlockHeaderInfo (header, confirmations.toLong, height.toLong, nextblockhash)
268+ })
269+ }
270+
210271 // ------------------------- FUNDING -------------------------//
211272
212273 def fundTransaction (tx : Transaction , options : FundTransactionOptions )(implicit ec : ExecutionContext ): Future [FundTransactionResponse ] = {
@@ -511,6 +572,10 @@ object BitcoinCoreClient {
511572
512573 case class Utxo (txid : ByteVector32 , amount : MilliBtc , confirmations : Long , safe : Boolean , label_opt : Option [String ])
513574
575+ case class TransactionInfo (tx : Transaction , confirmations : Int , blockId : Option [ByteVector32 ])
576+
577+ case class BlockHeaderInfo (header : BlockHeader , confirmation : Long , height : Long , nextBlockHash : Option [ByteVector32 ])
578+
514579 def toSatoshi (btcAmount : BigDecimal ): Satoshi = Satoshi (btcAmount.bigDecimal.scaleByPowerOfTen(8 ).longValue)
515580
516581}
0 commit comments