@@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc
18
18
19
19
import fr .acinq .bitcoin .scalacompat .Crypto .PublicKey
20
20
import fr .acinq .bitcoin .scalacompat ._
21
- import fr .acinq .bitcoin .{Bech32 , Block }
21
+ import fr .acinq .bitcoin .{Bech32 , Block , BlockHeader }
22
22
import fr .acinq .eclair .ShortChannelId .coordinates
23
23
import fr .acinq .eclair .blockchain .OnChainWallet
24
24
import fr .acinq .eclair .blockchain .OnChainWallet .{FundTransactionResponse , MakeFundingTxResponse , OnChainBalance , SignTransactionResponse }
@@ -74,6 +74,41 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
74
74
case t : JsonRPCError if t.error.code == - 5 => None // Invalid or non-wallet transaction id (code: -5)
75
75
}
76
76
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
+
77
112
/** Get the hash of the block containing a given transaction. */
78
113
private def getTxBlockHash (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [Option [ByteVector32 ]] =
79
114
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
207
242
case _ => Nil
208
243
}
209
244
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
+
210
271
// ------------------------- FUNDING -------------------------//
211
272
212
273
def fundTransaction (tx : Transaction , options : FundTransactionOptions )(implicit ec : ExecutionContext ): Future [FundTransactionResponse ] = {
@@ -511,6 +572,10 @@ object BitcoinCoreClient {
511
572
512
573
case class Utxo (txid : ByteVector32 , amount : MilliBtc , confirmations : Long , safe : Boolean , label_opt : Option [String ])
513
574
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
+
514
579
def toSatoshi (btcAmount : BigDecimal ): Satoshi = Satoshi (btcAmount.bigDecimal.scaleByPowerOfTen(8 ).longValue)
515
580
516
581
}
0 commit comments