1616
1717package fr .acinq .eclair .blockchain .bitcoind .rpc
1818
19+ import fr .acinq .bitcoin
1920import fr .acinq .bitcoin .scalacompat .Crypto .PublicKey
2021import fr .acinq .bitcoin .scalacompat ._
21- import fr .acinq .bitcoin .{Bech32 , Block }
22+ import fr .acinq .bitcoin .{Bech32 , Block , BlockHeader }
2223import fr .acinq .eclair .ShortChannelId .coordinates
2324import fr .acinq .eclair .blockchain .OnChainWallet
2425import fr .acinq .eclair .blockchain .OnChainWallet .{FundTransactionResponse , MakeFundingTxResponse , OnChainBalance , SignTransactionResponse }
@@ -28,10 +29,12 @@ import fr.acinq.eclair.transactions.Transactions
2829import fr .acinq .eclair .wire .protocol .ChannelAnnouncement
2930import fr .acinq .eclair .{BlockHeight , TimestampSecond , TxCoordinates }
3031import grizzled .slf4j .Logging
32+ import kotlin .Pair
3133import org .json4s .Formats
3234import org .json4s .JsonAST ._
3335import scodec .bits .ByteVector
3436
37+ import scala .collection .mutable
3538import scala .concurrent .{ExecutionContext , Future }
3639import scala .jdk .CollectionConverters .ListHasAsScala
3740import scala .util .{Failure , Success , Try }
@@ -74,6 +77,55 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
7477 case t : JsonRPCError if t.error.code == - 5 => None // Invalid or non-wallet transaction id (code: -5)
7578 }
7679
80+ def getTxConfirmationProof (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [List [BlockHeaderInfo ]] = {
81+ import KotlinUtils ._
82+
83+ /**
84+ * Scala wrapper for Block.verifyTxOutProof
85+ * @param proof tx output proof, as provided by bitcoind
86+ * @return a (Header, List(txhash, position)) tuple
87+ */
88+ def verifyTxOutProof (proof : ByteVector ): (BlockHeader , List [(ByteVector32 , Int )]) = {
89+ val check = Block .verifyTxOutProof(proof.toArray)
90+ (check.getFirst, check.getSecond.asScala.toList.map(p => (kmp2scala(p.getFirst), p.getSecond.intValue())))
91+ }
92+
93+ for {
94+ confirmations_opt <- getTxConfirmations(txid)
95+ if (confirmations_opt.isDefined && confirmations_opt.get > 0 )
96+ // get the merkle proof for our txid
97+ proof <- getTxOutProof(txid)
98+ // verify this merkle proof. if valid, we get the header for the block the tx was published in, and the tx hashes
99+ // that can be used to rebuild the block's merkle root
100+ (header, txHashesAndPos) = verifyTxOutProof(proof)
101+ // inclusionData contains a header and a list of (txid, position) that can be used to re-build the header's merkle root
102+ // check that the block hash included in the proof matches the block in which the tx was published
103+ Some (blockHash) <- getTxBlockHash(txid)
104+ _ = require(header.blockId.contentEquals(blockHash.toArray), " confirmation proof is not valid (block id mismatch)" )
105+ // check that our txid is included in the merkle root of the block it was published in
106+ txids = txHashesAndPos.map { case (txhash, _) => txhash.reverse }
107+ _ = require(txids.contains(txid), " confirmation proof is not valid (txid not found)" )
108+ // get the block in which our tx was confirmed and all following blocks
109+ headerInfos <- getBlockInfos(blockHash, confirmations_opt.get)
110+ _ = require(headerInfos.head.header.blockId.contentEquals(blockHash.toArray), " block header id mismatch" )
111+ _ = require(headerInfos.forall(hi => BlockHeader .checkProofOfWork(hi.header)), " header's proof of work is not valid" )
112+ } yield headerInfos
113+ }
114+
115+ def getTxOutProof (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [ByteVector ] =
116+ rpcClient.invoke(" gettxoutproof" , Array (txid)).collect { case JString (raw) => ByteVector .fromValidHex(raw) }
117+
118+ // returns a chain a blocks of a given size starting at `blockId`
119+ def getBlockInfos (blockId : ByteVector32 , count : Int )(implicit ec : ExecutionContext ): Future [List [BlockHeaderInfo ]] = {
120+ import KotlinUtils ._
121+
122+ def loop (blocks : List [BlockHeaderInfo ]): Future [List [BlockHeaderInfo ]] = if (blocks.size == count) Future .successful(blocks) else {
123+ getBlockHeaderInfo(blocks.last.nextBlockHash.get.reverse).flatMap(info => loop(blocks :+ info))
124+ }
125+
126+ getBlockHeaderInfo(blockId).flatMap(info => loop(List (info)))
127+ }
128+
77129 /** Get the hash of the block containing a given transaction. */
78130 private def getTxBlockHash (txid : ByteVector32 )(implicit ec : ExecutionContext ): Future [Option [ByteVector32 ]] =
79131 rpcClient.invoke(" getrawtransaction" , txid, 1 /* verbose output is needed to get the block hash */ )
@@ -207,6 +259,32 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
207259 case _ => Nil
208260 }
209261
262+ // ------------------------- BLOCKS -------------------------//
263+ def getBlockHash (height : Int )(implicit ec : ExecutionContext ): Future [ByteVector32 ] = {
264+ rpcClient.invoke(" getblockhash" , height).map(json => {
265+ val JString (hash) = json
266+ ByteVector32 .fromValidHex(hash)
267+ })
268+ }
269+
270+ def getBlockHeaderInfo (blockId : ByteVector32 )(implicit ec : ExecutionContext ): Future [BlockHeaderInfo ] = {
271+ import fr .acinq .bitcoin .{ByteVector32 => ByteVector32Kt }
272+ rpcClient.invoke(" getblockheader" , blockId.toString()).map(json => {
273+ val JInt (confirmations) = json \ " confirmations"
274+ val JInt (height) = json \ " height"
275+ val JInt (time) = json \ " time"
276+ val JInt (version) = json \ " version"
277+ val JInt (nonce) = json \ " nonce"
278+ val JString (bits) = json \ " bits"
279+ val merkleRoot = ByteVector32Kt .fromValidHex((json \ " merkleroot" ).extract[String ]).reversed()
280+ val previousblockhash = ByteVector32Kt .fromValidHex((json \ " previousblockhash" ).extract[String ]).reversed()
281+ val nextblockhash = (json \ " nextblockhash" ).extractOpt[String ].map(h => ByteVector32 .fromValidHex(h).reverse)
282+ val header = new BlockHeader (version.longValue, previousblockhash, merkleRoot, time.longValue, java.lang.Long .parseLong(bits, 16 ), nonce.longValue)
283+ require(header.blockId == KotlinUtils .scala2kmp(blockId))
284+ BlockHeaderInfo (header, confirmations.toLong, height.toLong, nextblockhash)
285+ })
286+ }
287+
210288 // ------------------------- FUNDING -------------------------//
211289
212290 def fundTransaction (tx : Transaction , options : FundTransactionOptions )(implicit ec : ExecutionContext ): Future [FundTransactionResponse ] = {
@@ -511,6 +589,10 @@ object BitcoinCoreClient {
511589
512590 case class Utxo (txid : ByteVector32 , amount : MilliBtc , confirmations : Long , safe : Boolean , label_opt : Option [String ])
513591
592+ case class TransactionInfo (tx : Transaction , confirmations : Int , blockId : Option [ByteVector32 ])
593+
594+ case class BlockHeaderInfo (header : BlockHeader , confirmation : Long , height : Long , nextBlockHash : Option [ByteVector32 ])
595+
514596 def toSatoshi (btcAmount : BigDecimal ): Satoshi = Satoshi (btcAmount.bigDecimal.scaleByPowerOfTen(8 ).longValue)
515597
516598}
0 commit comments