@@ -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,70 @@ 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+ /**
78+ *
79+ * @param txid transaction id
80+ * @return a list of block header information, starting from the block in which the transaction was published, up to the current tip
81+ */
82+ def getTxConfirmationProof (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [List [BlockHeaderInfo ]] = {
83+ import KotlinUtils ._
84+
85+ /**
86+ * Scala wrapper for Block.verifyTxOutProof
87+ *
88+ * @param proof tx output proof, as provided by bitcoind
89+ * @return a (Header, List(txhash, position)) tuple. Header is the header of the block used to compute the input proof, and
90+ * (txhash, position) is a list of transaction ids that were verified, and their position in the block
91+ */
92+ def verifyTxOutProof (proof : ByteVector ): (BlockHeader , List [(ByteVector32 , Int )]) = {
93+ val check = Block .verifyTxOutProof(proof.toArray)
94+ (check.getFirst, check.getSecond.asScala.toList.map(p => (kmp2scala(p.getFirst), p.getSecond.intValue())))
95+ }
96+
97+ for {
98+ confirmations_opt <- getTxConfirmations(txid)
99+ if (confirmations_opt.isDefined && confirmations_opt.get > 0 )
100+ // get the merkle proof for our txid
101+ proof <- getTxOutProof(txid)
102+ // verify this merkle proof. if valid, we get the header for the block the tx was published in, and the tx hashes
103+ // that can be used to rebuild the block's merkle root
104+ (header, txHashesAndPos) = verifyTxOutProof(proof)
105+ // 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)" )
112+ // 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
116+ }
117+
118+ def getTxOutProof (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [ByteVector ] =
119+ rpcClient.invoke(" gettxoutproof" , Array (txid)).collect { case JString (raw) => ByteVector .fromValidHex(raw) }
120+
121+ // returns a chain a blocks of a given size starting at `blockId`
122+ def getBlockInfos (blockId : ByteVector32 , count : Int )(implicit ec : ExecutionContext ): Future [List [BlockHeaderInfo ]] = {
123+ import KotlinUtils ._
124+
125+ def loop (blocks : List [BlockHeaderInfo ]): Future [List [BlockHeaderInfo ]] = if (blocks.size == count) Future .successful(blocks) else {
126+ getBlockHeaderInfo(blocks.last.nextBlockHash.get.reverse).flatMap(info => loop(blocks :+ info))
127+ }
128+
129+ getBlockHeaderInfo(blockId).flatMap(info => loop(List (info))).map(blocks => {
130+ for (i <- 0 until blocks.size - 1 ) {
131+ require(BlockHeader .checkProofOfWork(blocks(i).header))
132+ require(blocks(i).height == blocks(0 ).height + i)
133+ require(blocks(i).confirmation == blocks(0 ).confirmation - i)
134+ require(blocks(i).nextBlockHash.contains(kmp2scala(blocks(i + 1 ).header.hash)))
135+ require(blocks(i + 1 ).header.hashPreviousBlock == blocks(i).header.hash)
136+ }
137+ blocks
138+ })
139+ }
140+
77141 /** Get the hash of the block containing a given transaction. */
78142 private def getTxBlockHash (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [Option [ByteVector32 ]] =
79143 rpcClient.invoke(" getrawtransaction" , txid, 1 /* verbose output is needed to get the block hash */ )
@@ -207,6 +271,32 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
207271 case _ => Nil
208272 }
209273
274+ // ------------------------- BLOCKS -------------------------//
275+ def getBlockHash (height : Int )(implicit ec : ExecutionContext ): Future [ByteVector32 ] = {
276+ rpcClient.invoke(" getblockhash" , height).map(json => {
277+ val JString (hash) = json
278+ ByteVector32 .fromValidHex(hash)
279+ })
280+ }
281+
282+ def getBlockHeaderInfo (blockId : ByteVector32 )(implicit ec : ExecutionContext ): Future [BlockHeaderInfo ] = {
283+ import fr .acinq .bitcoin .{ByteVector32 => ByteVector32Kt }
284+ rpcClient.invoke(" getblockheader" , blockId.toString()).map(json => {
285+ val JInt (confirmations) = json \ " confirmations"
286+ val JInt (height) = json \ " height"
287+ val JInt (time) = json \ " time"
288+ val JInt (version) = json \ " version"
289+ val JInt (nonce) = json \ " nonce"
290+ val JString (bits) = json \ " bits"
291+ val merkleRoot = ByteVector32Kt .fromValidHex((json \ " merkleroot" ).extract[String ]).reversed()
292+ val previousblockhash = ByteVector32Kt .fromValidHex((json \ " previousblockhash" ).extract[String ]).reversed()
293+ val nextblockhash = (json \ " nextblockhash" ).extractOpt[String ].map(h => ByteVector32 .fromValidHex(h).reverse)
294+ val header = new BlockHeader (version.longValue, previousblockhash, merkleRoot, time.longValue, java.lang.Long .parseLong(bits, 16 ), nonce.longValue)
295+ require(header.blockId == KotlinUtils .scala2kmp(blockId))
296+ BlockHeaderInfo (header, confirmations.toLong, height.toLong, nextblockhash)
297+ })
298+ }
299+
210300 // ------------------------- FUNDING -------------------------//
211301
212302 def fundTransaction (tx : Transaction , options : FundTransactionOptions )(implicit ec : ExecutionContext ): Future [FundTransactionResponse ] = {
@@ -546,6 +636,10 @@ object BitcoinCoreClient {
546636
547637 case class Utxo (txid : ByteVector32 , amount : MilliBtc , confirmations : Long , safe : Boolean , label_opt : Option [String ])
548638
639+ case class TransactionInfo (tx : Transaction , confirmations : Int , blockId : Option [ByteVector32 ])
640+
641+ case class BlockHeaderInfo (header : BlockHeader , confirmation : Long , height : Long , nextBlockHash : Option [ByteVector32 ])
642+
549643 def toSatoshi (btcAmount : BigDecimal ): Satoshi = Satoshi (btcAmount.bigDecimal.scaleByPowerOfTen(8 ).longValue)
550644
551645}
0 commit comments