Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ eclair {
// - ignore: eclair will leave these utxos locked and start
startup-locked-utxos-behavior = "stop"
final-pubkey-refresh-delay = 3 seconds
min-difficulty-target = 387294044 // difficulty of block 600000
}

node-alias = "eclair"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, RealShortChannelId, T
import java.util.concurrent.atomic.AtomicLong
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
import scala.util.{Failure, Success, Try}

/**
* Created by PM on 21/02/2016.
Expand Down Expand Up @@ -415,23 +415,31 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client

private def checkConfirmed(w: WatchConfirmed[_ <: WatchConfirmedTriggered]): Future[Unit] = {
log.debug("checking confirmations of txid={}", w.txId)
// NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really
// matter because this only happens once, when the watched transaction has reached min_depth

val minDifficultyTarget = nodeParams.chainHash match {
case Block.LivenetGenesisBlock.hash => Try(context.system.settings.config.getLong("eclair.bitcoind.min-difficulty-target")).getOrElse(0x1715a35cL) // 0x1715a35cL = 387294044 = difficulty target of block 600000
case Block.TestnetGenesisBlock.hash => 0x1d00ffffL
case _ => 0x207fffffL
}

client.getTxConfirmations(w.txId).flatMap {
case Some(confirmations) if confirmations >= w.minDepth =>
client.getTransaction(w.txId).flatMap { tx =>
client.getTransactionShortId(w.txId).map {
case (height, index) => w match {
case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx))
case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx))
case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx))
case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx))
case w: WatchAlternativeCommitTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchAlternativeCommitTxConfirmedTriggered(height, index, tx))
}
for {
proof <- client.getTxConfirmationProof(w.txId)
_ = require(proof.confirmations >= confirmations)
_ = require(proof.headerInfos.forall(hi => hi.header.bits <= minDifficultyTarget))
height = BlockHeight(proof.height)
tx <- client.getTransaction(w.txId)
} yield {
w match {
case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, proof.txIndex, tx))
case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, proof.txIndex, tx))
case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, proof.txIndex, tx))
case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, proof.txIndex, tx))
case w: WatchAlternativeCommitTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchAlternativeCommitTxConfirmedTriggered(height, proof.txIndex, tx))
}
}
case _ => Future.successful((): Unit)
case _ => Future.successful(())
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc

import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.bitcoin.{Bech32, Block}
import fr.acinq.bitcoin.{Bech32, Block, BlockHeader}
import fr.acinq.eclair.ShortChannelId.coordinates
import fr.acinq.eclair.blockchain.OnChainWallet
import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse}
Expand Down Expand Up @@ -52,7 +52,11 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
//------------------------- TRANSACTIONS -------------------------//

def getTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] =
getRawTransaction(txid).map(raw => Transaction.read(raw))
getRawTransaction(txid).map(raw => {
val tx = Transaction.read(raw)
require(tx.txid == txid, "transaction id mismatch")
tx
})

private def getRawTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("getrawtransaction", txid).collect {
Expand All @@ -66,21 +70,107 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
JInt(timestamp) = blockchainInfo \ "mediantime"
} yield GetTxWithMetaResponse(txid, tx_opt, TimestampSecond(timestamp.toLong))

/** Get the number of confirmations of a given transaction. */
def getTxConfirmations(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[Int]] =
rpcClient.invoke("getrawtransaction", txid, 1 /* verbose output is needed to get the number of confirmations */)
.map(json => Some((json \ "confirmations").extractOrElse[Int](0)))

def getTxInfo(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[TransactionInfo]] =
rpcClient.invoke("getrawtransaction", txid, 1).map(json => {
val confirmations = Some((json \ "confirmations").extractOrElse[Int](0))
val blockHash = (json \ "blockhash").extractOpt[String].map(ByteVector32.fromValidHex)
val hex = (json \ "hex").extract[String]
val tx = Transaction.read(hex)
require(tx.txid == txid, "transaction id mismatch")
Some(TransactionInfo(tx, confirmations, blockHash))
})
.recover {
case t: JsonRPCError if t.error.code == -5 => None // Invalid or non-wallet transaction id (code: -5)
}

/** Get the hash of the block containing a given transaction. */
private def getTxBlockHash(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[ByteVector32]] =
rpcClient.invoke("getrawtransaction", txid, 1 /* verbose output is needed to get the block hash */)
.map(json => (json \ "blockhash").extractOpt[String].map(ByteVector32.fromValidHex))
.recover {
case t: JsonRPCError if t.error.code == -5 => None // Invalid or non-wallet transaction id (code: -5)
def getTxConfirmations(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[Int]] = getTxInfo(txid).map(_.flatMap(_.confirmations))

/**
*
* @param txid transaction id
* @return a list of block header information, starting from the block in which the transaction was published, up to the current tip
*/
def getTxConfirmationProof(txid: ByteVector32)(implicit ec: ExecutionContext): Future[TxConfirmationProof] = {
import KotlinUtils._

/**
* Scala wrapper for Block.verifyTxOutProof
*
* @param proof tx output proof, as provided by bitcoind
* @return a (Header, List(txhash, position)) tuple. Header is the header of the block used to compute the input proof, and
* (txhash, position) is a list of transaction ids that were verified, and their position in the block
*/
def verifyTxOutProof(proof: ByteVector): (BlockHeader, List[(ByteVector32, Int)]) = {
val check = Block.verifyTxOutProof(proof.toArray)
(check.getFirst, check.getSecond.asScala.toList.map(p => (kmp2scala(p.getFirst), p.getSecond.intValue())))
}

for {
txinfo <- getTxInfo(txid)
confirmations_opt = txinfo.flatMap(_.confirmations)
if confirmations_opt.isDefined && confirmations_opt.get > 0
blockId = txinfo.flatMap(_.blockHash).get
// get the coinbase tx for this block, we'll use it to check the block's height
blockTxids <- getTransactionIds(blockId)
coinbaseTxid = blockTxids.head
coinbaseTx <- getTransaction(coinbaseTxid)
// get the merkle proof for our txid
proof <- getTxOutProof(Seq(txid, coinbaseTxid), Some(blockId))
// verify this merkle proof. if valid, we get the header for the block the tx was published in, and the tx hashes
// that can be used to rebuild the block's merkle root
(header, txHashesAndPos) = verifyTxOutProof(proof)
_ = require(txHashesAndPos.exists { case (hash, _) => hash.reverse == coinbaseTxid }, "confirmation proof is not valid for coinbase tx")
// inclusionData contains a header and a list of (txid, position) that can be used to re-build the header's merkle root
// find the position of txid in the merkle root of the block it was published in
pos_opt = txHashesAndPos.find { case (hash, _) => hash.reverse == txid } map { case (_, pos) => pos }
_ = require(pos_opt.isDefined, "confirmation proof is not valid (txid not found)")
// get the block in which our tx was confirmed and all following blocks
height = decodeBlockHeight(coinbaseTx)
headerInfos <- getBlockInfos(header.blockId, confirmations_opt.get)
_ = require(headerInfos.head.header.blockId == header.blockId, "block header id mismatch")
_ = require(headerInfos.head.height == height, "block header height mismatch")
} yield TxConfirmationProof(txid, headerInfos, pos_opt.get)
}

def getTxOutProof(txids: Seq[ByteVector32], blockHash_opt: Option[ByteVector32])(implicit ec: ExecutionContext): Future[ByteVector] = (blockHash_opt match {
case Some(blockHash) => rpcClient.invoke("gettxoutproof", txids.toArray, blockHash)
case None => rpcClient.invoke("gettxoutproof", txids.toArray)
}).collect { case JString(raw) => ByteVector.fromValidHex(raw) }

def getTxOutProof(txid: ByteVector32, blockHash_opt: Option[ByteVector32] = None)(implicit ec: ExecutionContext): Future[ByteVector] = getTxOutProof(Seq(txid), blockHash_opt)

def getBlock(blockId: ByteVector32)(implicit ec: ExecutionContext): Future[Block] = {
rpcClient.invoke("getblock", blockId.toString(), 0).collect {
case JString(raw) =>
val block = Block.read(raw)
require(block.blockId.contentEquals(blockId.toArray))
block
}
}

// returns a chain a blocks of a given size starting at `blockId`
def getBlockInfos(blockId: ByteVector32, count: Int)(implicit ec: ExecutionContext): Future[List[BlockHeaderInfo]] = {
import KotlinUtils._

def loop(blocks: List[BlockHeaderInfo]): Future[List[BlockHeaderInfo]] = if (blocks.size == count) Future.successful(blocks) else {
getBlockHeaderInfo(blocks.last.nextBlockId.get).flatMap(info => loop(blocks :+ info))
}

getBlockHeaderInfo(blockId).flatMap(info => loop(List(info))).map(blocks => {
for (i <- 0 until blocks.size - 1) {
require(BlockHeader.checkProofOfWork(blocks(i).header))
require(blocks(i).height == blocks(0).height + i)
require(blocks(i).confirmation == blocks(0).confirmation - i)
require(blocks(i).nextBlockId.contains(kmp2scala(blocks(i + 1).header.blockId)), "next block id mismatch")
require(blocks(i + 1).header.hashPreviousBlock == blocks(i).header.hash, "previous block id mismatch")
}
blocks
})
}

/** Get the hash of the block containing a given transaction. */
private def getTxBlockHash(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[ByteVector32]] = getTxInfo(txid).map(i => i.flatMap(_.blockHash))

/**
* @return a Future[height, index] where height is the height of the block where this transaction was published, and
Expand Down Expand Up @@ -207,6 +297,39 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
case _ => Nil
}

//------------------------- BLOCKS -------------------------//
def getBlockHash(height: Int)(implicit ec: ExecutionContext): Future[ByteVector32] = {
rpcClient.invoke("getblockhash", height).map(json => {
val JString(hash) = json
ByteVector32.fromValidHex(hash)
})
}

def getBlockHeaderInfo(blockId: ByteVector32)(implicit ec: ExecutionContext): Future[BlockHeaderInfo] = {
import fr.acinq.bitcoin.{ByteVector32 => ByteVector32Kt}
rpcClient.invoke("getblockheader", blockId.toString()).map(json => {
val JInt(confirmations) = json \ "confirmations"
val JInt(height) = json \ "height"
val JInt(time) = json \ "time"
val JInt(version) = json \ "version"
val JInt(nonce) = json \ "nonce"
val JString(bits) = json \ "bits"
val merkleRoot = ByteVector32Kt.fromValidHex((json \ "merkleroot").extract[String]).reversed()
val previousblockId = ByteVector32Kt.fromValidHex((json \ "previousblockhash").extract[String])
val nextblockId = (json \ "nextblockhash").extractOpt[String].map(h => ByteVector32.fromValidHex(h))
val header = new BlockHeader(version.longValue, previousblockId.reversed(), merkleRoot, time.longValue, java.lang.Long.parseLong(bits, 16), nonce.longValue)
require(header.blockId == KotlinUtils.scala2kmp(blockId))
BlockHeaderInfo(header, confirmations.toLong, height.toLong, nextblockId)
})
}

def getTransactionIds(blockId: ByteVector32)(implicit ec: ExecutionContext): Future[Seq[ByteVector32]] = {
rpcClient.invoke("getblock", blockId.toString(), 1).map(json => {
val JArray(txids) = json \ "tx"
txids.map(txid => ByteVector32.fromValidHex(txid.extract[String]))
})
}

//------------------------- FUNDING -------------------------//

def fundTransaction(tx: Transaction, options: FundTransactionOptions)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
Expand Down Expand Up @@ -623,6 +746,35 @@ object BitcoinCoreClient {

case class Utxo(txid: ByteVector32, amount: MilliBtc, confirmations: Long, safe: Boolean, label_opt: Option[String])

case class TransactionInfo(tx: Transaction, confirmations: Option[Int], blockHash: Option[ByteVector32])

case class BlockHeaderInfo(header: BlockHeader, confirmation: Long, height: Long, nextBlockId: Option[ByteVector32])

case class BlockInfo(headerInfo: BlockHeaderInfo, txids: Seq[ByteVector32])

case class TxConfirmationProof(txid: ByteVector32, headerInfos: List[BlockHeaderInfo], txIndex: Int) {
val confirmations = headerInfos.size
val height = headerInfos.head.height
}

def toSatoshi(btcAmount: BigDecimal): Satoshi = Satoshi(btcAmount.bigDecimal.scaleByPowerOfTen(8).longValue)

/**
* TODO: move this into bitcoin-kmp
* Extract the block height embedded in a block's coinbase tx (see BIP34)
*
* @param signatureScript coinbase transaction input signature script
* @return the block height committed to in the coinbase tx
*/
def decodeBlockHeight(signatureScript: ByteVector): Long = {
import fr.acinq.bitcoin.scalacompat.KotlinUtils._

val it = fr.acinq.bitcoin.Script.INSTANCE.scriptIterator(signatureScript.toArray)
val op: ScriptElt = it.next()
require(op.isPush, "signature script does not match BIP34 rules")
Script.decodeNumber(op.asInstanceOf[OP_PUSHDATA].data, false, 4)
}

def decodeBlockHeight(coinbaseTx: Transaction): Long = decodeBlockHeight(coinbaseTx.txIn(0).signatureScript)

}
Loading