@@ -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,70 @@ 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
+ /**
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
+
77
141
/** Get the hash of the block containing a given transaction. */
78
142
private def getTxBlockHash (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [Option [ByteVector32 ]] =
79
143
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
207
271
case _ => Nil
208
272
}
209
273
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
+
210
300
// ------------------------- FUNDING -------------------------//
211
301
212
302
def fundTransaction (tx : Transaction , options : FundTransactionOptions )(implicit ec : ExecutionContext ): Future [FundTransactionResponse ] = {
@@ -623,6 +713,10 @@ object BitcoinCoreClient {
623
713
624
714
case class Utxo (txid : ByteVector32 , amount : MilliBtc , confirmations : Long , safe : Boolean , label_opt : Option [String ])
625
715
716
+ case class TransactionInfo (tx : Transaction , confirmations : Int , blockId : Option [ByteVector32 ])
717
+
718
+ case class BlockHeaderInfo (header : BlockHeader , confirmation : Long , height : Long , nextBlockHash : Option [ByteVector32 ])
719
+
626
720
def toSatoshi (btcAmount : BigDecimal ): Satoshi = Satoshi (btcAmount.bigDecimal.scaleByPowerOfTen(8 ).longValue)
627
721
628
722
}
0 commit comments