From 8684fb238bbe769d53ad0cddfe364bb27f8e0e05 Mon Sep 17 00:00:00 2001 From: Dominique Date: Tue, 20 Feb 2018 14:26:38 +0100 Subject: [PATCH] Removed BitcoinJ watcher (#447) Added guava dependency which was previously bundled with bitcoinj. --- eclair-core/pom.xml | 10 +- eclair-core/src/main/resources/reference.conf | 11 +- .../scala/fr/acinq/eclair/NodeParams.scala | 3 - .../main/scala/fr/acinq/eclair/Setup.scala | 17 +- .../eclair/blockchain/WatcherTypes.scala | 2 +- .../blockchain/bitcoinj/BitcoinjKit.scala | 152 ---- .../blockchain/bitcoinj/BitcoinjWallet.scala | 68 -- .../blockchain/bitcoinj/BitcoinjWatcher.scala | 191 ----- .../fr/acinq/eclair/channel/Channel.scala | 34 +- .../bitcoind/BitcoinCoreWalletSpec.scala | 1 - .../blockchain/bitcoinj/BitcoinjSpec.scala | 217 ------ .../BasicBitcoinjIntegrationSpec.scala | 661 ------------------ .../gui/controllers/MainController.scala | 3 +- pom.xml | 2 +- 14 files changed, 17 insertions(+), 1355 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjKit.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWallet.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWatcher.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/BasicBitcoinjIntegrationSpec.scala diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index f3d0a5ee2..c9ac3b0bc 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -143,11 +143,6 @@ jeromq 0.4.0 - - fr.acinq - bitcoinj-core - ${bitcoinj.version} - org.scodec @@ -188,6 +183,11 @@ jsr305 3.0.2 + + com.google.guava + guava + ${guava.version} + com.typesafe.akka diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index bf1afad27..6f18ffd01 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -15,7 +15,7 @@ eclair { password = "" // password for basic auth, must be non empty if json-rpc api is enabled } - watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum" + watcher-type = "bitcoind" // other *experimental* values include "electrum" bitcoind { host = "localhost" @@ -25,15 +25,6 @@ eclair { zmq = "tcp://127.0.0.1:29000" } - bitcoinj { - static-peers = [ - #{ // currently used in integration tests to override default port - # host = "localhost" - # port = 28333 - #} - ] - } - default-feerates { // those are in satoshis per byte delay-blocks { 1 = 210 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index e86c0ab6c..ad2b71016 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -67,8 +67,6 @@ object NodeParams { object BITCOIND extends WatcherType - object BITCOINJ extends WatcherType - object ELECTRUM extends WatcherType /** @@ -123,7 +121,6 @@ object NodeParams { require(color.size == 3, "color should be a 3-bytes hex buffer") val watcherType = config.getString("watcher-type") match { - case "bitcoinj" => BITCOINJ case "electrum" => ELECTRUM case _ => BITCOIND } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index af1c437fb..042c8d23c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -10,12 +10,11 @@ import akka.stream.{ActorMaterializer, BindFailedException} import akka.util.Timeout import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.{BinaryData, Block} -import fr.acinq.eclair.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM} +import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} import fr.acinq.eclair.api.{GetInfoResponse, Service} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher} -import fr.acinq.eclair.blockchain.bitcoinj.{BitcoinjKit, BitcoinjWallet, BitcoinjWatcher} import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumEclairWallet, ElectrumWallet, ElectrumWatcher} import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _} import fr.acinq.eclair.blockchain.{EclairWallet, _} @@ -25,7 +24,6 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ import grizzled.slf4j.Logging -import scala.collection.JavaConversions._ import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future, Promise} @@ -92,14 +90,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act // TODO: add a check on bitcoin version? Bitcoind(bitcoinClient) - case BITCOINJ => - logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!") - val staticPeers = config.getConfigList("bitcoinj.static-peers").map(c => new InetSocketAddress(c.getString("host"), c.getInt("port"))).toList - logger.info(s"using staticPeers=$staticPeers") - val bitcoinjKit = new BitcoinjKit(chain, datadir, staticPeers) - bitcoinjKit.startAsync() - Await.ready(bitcoinjKit.initialized, 10 seconds) - Bitcoinj(bitcoinjKit) case ELECTRUM => logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!") val addressesFile = chain match { @@ -137,9 +127,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act case Bitcoind(bitcoinClient) => system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart)) system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(bitcoinClient), "watcher", SupervisorStrategy.Resume)) - case Bitcoinj(bitcoinj) => - zmqConnected.success(true) - system.actorOf(SimpleSupervisor.props(BitcoinjWatcher.props(bitcoinj), "watcher", SupervisorStrategy.Resume)) case Electrum(electrumClient) => zmqConnected.success(true) system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume)) @@ -147,7 +134,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act val wallet = bitcoin match { case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient) - case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet())) case Electrum(electrumClient) => seed_opt match { case Some(seed) => val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(Block.TestnetGenesisBlock.hash)), "electrum-wallet") new ElectrumEclairWallet(electrumWallet) @@ -223,7 +209,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act // @formatter:off sealed trait Bitcoin case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin -case class Bitcoinj(bitcoinjKit: BitcoinjKit) extends Bitcoin case class Electrum(electrumClient: ActorRef) extends Bitcoin // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala index cad729c24..1f2951789 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala @@ -18,7 +18,7 @@ sealed trait Watch { def channel: ActorRef def event: BitcoinEvent } -// we need a public key script to use bitcoinj or electrum apis +// we need a public key script to use electrum apis final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, publicKeyScript: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch object WatchConfirmed { // if we have the entire transaction, we can get the redeemScript from the witness, and re-compute the publicKeyScript diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjKit.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjKit.scala deleted file mode 100644 index 3c110f66e..000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjKit.scala +++ /dev/null @@ -1,152 +0,0 @@ -package fr.acinq.eclair.blockchain.bitcoinj - -import java.io.File -import java.net.InetSocketAddress - -import akka.actor.ActorSystem -import com.google.common.util.concurrent.{FutureCallback, Futures} -import fr.acinq.bitcoin.Transaction -import fr.acinq.eclair.Globals -import fr.acinq.eclair.blockchain.CurrentBlockCount -import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjKit._ -import grizzled.slf4j.Logging -import org.bitcoinj.core.TransactionConfidence.ConfidenceType -import org.bitcoinj.core.listeners._ -import org.bitcoinj.core.{Block, Context, FilteredBlock, NetworkParameters, Peer, PeerAddress, StoredBlock, VersionMessage, Transaction => BitcoinjTransaction} -import org.bitcoinj.kits.WalletAppKit -import org.bitcoinj.params.{RegTestParams, TestNet3Params} -import org.bitcoinj.utils.Threading -import org.bitcoinj.wallet.Wallet - -import scala.collection.JavaConversions._ -import scala.concurrent.Promise -import scala.util.Try - -/** - * Created by PM on 09/07/2017. - */ -class BitcoinjKit(chain: String, datadir: File, staticPeers: List[InetSocketAddress] = Nil)(implicit system: ActorSystem) extends WalletAppKit(chain2Params(chain), datadir, "bitcoinj", true) with Logging { - - if (staticPeers.size > 0) { - logger.info(s"using staticPeers=${staticPeers.mkString(",")}") - setPeerNodes(staticPeers.map(addr => new PeerAddress(params, addr)).head) - } - - // tells us when the peerGroup/chain/wallet are accessible - private val initializedPromise = Promise[Boolean]() - val initialized = initializedPromise.future - - // tells us as soon as we know the current block height - private val atCurrentHeightPromise = Promise[Boolean]() - val atCurrentHeight = atCurrentHeightPromise.future - - // tells us when we are at current block height - // private val syncedPromise = Promise[Boolean]() - // val synced = syncedPromise.future - - private def updateBlockCount(blockCount: Int) = { - // when synchronizing we don't want to advertise previous blocks - if (Globals.blockCount.get() < blockCount) { - logger.debug(s"current blockchain height=$blockCount") - system.eventStream.publish(CurrentBlockCount(blockCount)) - Globals.blockCount.set(blockCount) - } - } - - override def onSetupCompleted(): Unit = { - - logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}") - logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}") - - peerGroup().setMinRequiredProtocolVersion(70015) // bitcoin core 0.13 - wallet().watchMode = true - - // setDownloadListener(new DownloadProgressTracker { - // override def doneDownload(): Unit = { - // super.doneDownload() - // // may be called multiple times - // syncedPromise.trySuccess(true) - // } - // }) - - // we set the blockcount to the previous stored block height - updateBlockCount(chain().getBestChainHeight) - - // as soon as we are connected the peers will tell us their current height and we will advertise it immediately - peerGroup().addConnectedEventListener(new PeerConnectedEventListener { - override def onPeerConnected(peer: Peer, peerCount: Int): Unit = { - if ((peer.getPeerVersionMessage.localServices & VersionMessage.NODE_WITNESS) == 0) { - peer.close() - } else { - Context.propagate(wallet.getContext) - // we wait for at least 3 peers before relying on the information they are giving, but we trust localhost - if (peer.getAddress.getAddr.isLoopbackAddress || peerCount > 3) { - updateBlockCount(peerGroup().getMostCommonChainHeight) - // may be called multiple times - atCurrentHeightPromise.trySuccess(true) - } - } - } - }) - - peerGroup.addBlocksDownloadedEventListener(new BlocksDownloadedEventListener { - override def onBlocksDownloaded(peer: Peer, block: Block, filteredBlock: FilteredBlock, blocksLeft: Int): Unit = { - Context.propagate(wallet.getContext) - logger.debug(s"received block=${block.getHashAsString} (size=${block.bitcoinSerialize().size} txs=${Try(block.getTransactions.size).getOrElse(-1)}) filteredBlock=${Try(filteredBlock.getHash.toString).getOrElse("N/A")} (size=${Try(block.bitcoinSerialize().size).getOrElse(-1)} txs=${Try(filteredBlock.getTransactionCount).getOrElse(-1)})") - Try { - if (filteredBlock.getAssociatedTransactions.size() > 0) { - logger.info(s"retrieving full block ${block.getHashAsString}") - Futures.addCallback(peer.getBlock(block.getHash), new FutureCallback[Block] { - override def onFailure(throwable: Throwable) = logger.error(s"could not retrieve full block=${block.getHashAsString}") - - override def onSuccess(fullBlock: Block) = { - Try { - Context.propagate(wallet.getContext) - fullBlock.getTransactions.foreach { - case tx => - logger.debug(s"received tx=${tx.getHashAsString} witness=${Transaction.read(tx.bitcoinSerialize()).txIn(0).witness.stack.size} from fullBlock=${fullBlock.getHash} confidence=${tx.getConfidence}") - val depthInBlocks = tx.getConfidence.getConfidenceType match { - case ConfidenceType.DEAD => -1 - case _ => tx.getConfidence.getDepthInBlocks - } - system.eventStream.publish(NewConfidenceLevel(Transaction.read(tx.bitcoinSerialize()), 0, depthInBlocks)) - } - } - } - }, Threading.USER_THREAD) - } - } - } - }) - - chain().addNewBestBlockListener(new NewBestBlockListener { - override def notifyNewBestBlock(storedBlock: StoredBlock): Unit = - updateBlockCount(storedBlock.getHeight) - }) - - wallet().addTransactionConfidenceEventListener(new TransactionConfidenceEventListener { - override def onTransactionConfidenceChanged(wallet: Wallet, bitcoinjTx: BitcoinjTransaction): Unit = { - Context.propagate(wallet.getContext) - val tx = Transaction.read(bitcoinjTx.bitcoinSerialize()) - logger.info(s"tx confidence changed for txid=${tx.txid} confidence=${bitcoinjTx.getConfidence} witness=${bitcoinjTx.getWitness(0)}") - val (blockHeight, confirmations) = bitcoinjTx.getConfidence.getConfidenceType match { - case ConfidenceType.DEAD => (-1, -1) - case ConfidenceType.BUILDING => (bitcoinjTx.getConfidence.getAppearedAtChainHeight, bitcoinjTx.getConfidence.getDepthInBlocks) - case _ => (-1, bitcoinjTx.getConfidence.getDepthInBlocks) - } - system.eventStream.publish(NewConfidenceLevel(tx, blockHeight, confirmations)) - } - }) - - initializedPromise.success(true) - } - -} - -object BitcoinjKit { - - def chain2Params(chain: String): NetworkParameters = chain match { - case "regtest" => RegTestParams.get() - case "test" => TestNet3Params.get() - } -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWallet.scala deleted file mode 100644 index eb67af80b..000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWallet.scala +++ /dev/null @@ -1,68 +0,0 @@ -package fr.acinq.eclair.blockchain.bitcoinj - -import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction} -import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse} -import grizzled.slf4j.Logging -import org.bitcoinj.core.{Coin, Context, Transaction => BitcoinjTransaction} -import org.bitcoinj.script.Script -import org.bitcoinj.wallet.{SendRequest, Wallet} - -import scala.collection.JavaConversions._ -import scala.concurrent.{ExecutionContext, Future} - -/** - * Created by PM on 08/07/2017. - */ -class BitcoinjWallet(val fWallet: Future[Wallet])(implicit ec: ExecutionContext) extends EclairWallet with Logging { - - fWallet.map(wallet => wallet.allowSpendingUnconfirmedTransactions()) - - override def getBalance: Future[Satoshi] = for { - wallet <- fWallet - } yield { - Context.propagate(wallet.getContext) - Satoshi(wallet.getBalance.longValue()) - } - - override def getFinalAddress: Future[String] = for { - wallet <- fWallet - } yield { - Context.propagate(wallet.getContext) - wallet.currentReceiveAddress().toBase58 - } - - override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = for { - wallet <- fWallet - } yield { - logger.info(s"building funding tx") - Context.propagate(wallet.getContext) - val script = new Script(pubkeyScript) - val tx = new BitcoinjTransaction(wallet.getParams) - tx.addOutput(Coin.valueOf(amount.amount), script) - val req = SendRequest.forTx(tx) - wallet.completeTx(req) - val txOutputIndex = tx.getOutputs.find(_.getScriptPubKey.equals(script)).get.getIndex - MakeFundingTxResponse(Transaction.read(tx.bitcoinSerialize()), txOutputIndex) - } - - override def commit(tx: Transaction): Future[Boolean] = { - // we make sure that we haven't double spent our own tx (eg by opening 2 channels at the same time) - val serializedTx = Transaction.write(tx) - logger.info(s"committing tx: txid=${tx.txid} tx=$serializedTx") - for { - wallet <- fWallet - _ = Context.propagate(wallet.getContext) - bitcoinjTx = new org.bitcoinj.core.Transaction(wallet.getParams(), serializedTx) - canCommit = wallet.maybeCommitTx(bitcoinjTx) - _ = logger.info(s"commit txid=${tx.txid} result=$canCommit") - } yield canCommit - } - - /** - * There are no locks on bitcoinj, this is a no-op - * - * @param tx - * @return - */ - override def rollback(tx: Transaction) = Future.successful(true) -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWatcher.scala deleted file mode 100644 index 7080ac155..000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjWatcher.scala +++ /dev/null @@ -1,191 +0,0 @@ -package fr.acinq.eclair.blockchain.bitcoinj - -import akka.actor.{Actor, ActorLogging, Props, Terminated} -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.{FutureCallback, Futures} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.{Globals, fromShortId} -import org.bitcoinj.core.{Context, Transaction => BitcoinjTransaction} -import org.bitcoinj.kits.WalletAppKit -import org.bitcoinj.script.Script - -import scala.collection.SortedMap -import scala.concurrent.ExecutionContext -import scala.util.{Failure, Success, Try} - -final case class NewConfidenceLevel(tx: Transaction, blockHeight: Int, confirmations: Int) extends BlockchainEvent - -/** - * A blockchain watcher that: - * - receives bitcoin events (new blocks and new txes) directly from the bitcoin network - * - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs) - * Created by PM on 21/02/2016. - */ -class BitcoinjWatcher(val kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { - - context.system.eventStream.subscribe(self, classOf[BlockchainEvent]) - context.system.eventStream.subscribe(self, classOf[NewConfidenceLevel]) - - val broadcaster = context.actorOf(Props(new Broadcaster(kit: WalletAppKit)), name = "broadcaster") - - case class TriggerEvent(w: Watch, e: WatchEvent) - - def receive: Receive = watching(Set(), SortedMap(), Nil, Nil) - - def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], oldEvents: Seq[NewConfidenceLevel], sent: Seq[TriggerEvent]): Receive = { - - case event@NewConfidenceLevel(tx, blockHeight, confirmations) => - log.debug(s"analyzing txid=${tx.txid} confirmations=$confirmations tx=$tx") - watches.collect { - case w@WatchSpentBasic(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) => - self ! TriggerEvent(w, WatchEventSpentBasic(event)) - case w@WatchSpent(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) => - self ! TriggerEvent(w, WatchEventSpent(event, tx)) - case w@WatchConfirmed(_, txId, _, minDepth, event) if txId == tx.txid && confirmations >= minDepth => - self ! TriggerEvent(w, WatchEventConfirmed(event, blockHeight, 0)) - } - context become watching(watches, block2tx, oldEvents.filterNot(_.tx.txid == tx.txid) :+ event, sent) - - case t@TriggerEvent(w, e) if watches.contains(w) && !sent.contains(t) => - log.info(s"triggering $w") - w.channel ! e - // NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx - // They are never cleaned up but it is not a big deal for now (1 channel == 1 watch) - val newWatches = if (!w.isInstanceOf[WatchSpent]) watches - w else watches - context.become(watching(newWatches, block2tx, oldEvents, sent :+ t)) - - case CurrentBlockCount(count) => { - val toPublish = block2tx.filterKeys(_ <= count) - toPublish.values.flatten.map(tx => publish(tx)) - context.become(watching(watches, block2tx -- toPublish.keys, oldEvents, sent)) - } - - case w: Watch if !watches.contains(w) => - w match { - case w: WatchConfirmed => addHint(w.publicKeyScript) - case w: WatchSpent => addHint(w.publicKeyScript) - case w: WatchSpentBasic => addHint(w.publicKeyScript) - case _ => () - } - log.debug(s"adding watch $w for $sender") - log.info(s"resending ${oldEvents.size} events!") - oldEvents.foreach(self ! _) - context.watch(w.channel) - context.become(watching(watches + w, block2tx, oldEvents, sent)) - - case PublishAsap(tx) => - val blockCount = Globals.blockCount.get() - val cltvTimeout = Scripts.cltvTimeout(tx) - val csvTimeout = Scripts.csvTimeout(tx) - if (csvTimeout > 0) { - require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs") - val parentTxid = tx.txIn(0).outPoint.txid - log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=$tx") - val parentPublicKey = fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wsh(tx.txIn.head.witness.stack.last)) - self ! WatchConfirmed(self, parentTxid, parentPublicKey, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx)) - } else if (cltvTimeout > blockCount) { - log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)") - val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx) - context.become(watching(watches, block2tx1, oldEvents, sent)) - } else publish(tx) - - case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) => - log.info(s"parent tx of txid=${tx.txid} has been confirmed") - val blockCount = Globals.blockCount.get() - val csvTimeout = Scripts.csvTimeout(tx) - val absTimeout = blockHeight + csvTimeout - if (absTimeout > blockCount) { - log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)") - val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx) - context.become(watching(watches, block2tx1, oldEvents, sent)) - } else publish(tx) - - case ValidateRequest(c) => - log.info(s"blindly validating channel=$c") - val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) - val (_, _, outputIndex) = fromShortId(c.shortChannelId) - val fakeFundingTx = Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format - lockTime = 0) - sender ! ValidateResult(c, Some(fakeFundingTx), true, None) - - case Terminated(channel) => - // we remove watches associated to dead actor - val deprecatedWatches = watches.filter(_.channel == channel) - context.become(watching(watches -- deprecatedWatches, block2tx, oldEvents, sent)) - - case 'watches => sender ! watches - - } - - /** - * Bitcoinj needs hints to be able to detect transactions - * - * @param pubkeyScript - * @return - */ - def addHint(pubkeyScript: BinaryData) = { - Context.propagate(kit.wallet.getContext) - val script = new Script(pubkeyScript) - // set creation time to 2017/09/01, so bitcoinj can still use its checkpoints optimizations - script.setCreationTimeSeconds(1501538400L) // 2017-09-01 - kit.wallet().addWatchedScripts(ImmutableList.of(script)) - } - - def publish(tx: Transaction): Unit = broadcaster ! tx - -} - -object BitcoinjWatcher { - - def props(kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new BitcoinjWatcher(kit)(ec)) - -} - -class Broadcaster(kit: WalletAppKit) extends Actor with ActorLogging { - - override def receive: Receive = { - case tx: Transaction => - broadcast(tx) - context become waiting(Nil) - } - - def waiting(stash: Seq[Transaction]): Receive = { - case BroadcastResult(tx, result) => - result match { - case Success(_) => log.info(s"broadcast success for txid=${tx.txid}") - case Failure(t) => log.error(t, s"broadcast failure for txid=${tx.txid}: ") - } - stash match { - case head :: rest => - broadcast(head) - context become waiting(rest) - case Nil => context become receive - } - case tx: Transaction => - log.info(s"stashing txid=${tx.txid} for broadcast") - context become waiting(stash :+ tx) - } - - case class BroadcastResult(tx: Transaction, result: Try[Boolean]) - - def broadcast(tx: Transaction) = { - Context.propagate(kit.wallet().getContext) - val bitcoinjTx = new org.bitcoinj.core.Transaction(kit.wallet().getParams, Transaction.write(tx)) - log.info(s"broadcasting txid=${tx.txid}") - Futures.addCallback(kit.peerGroup().broadcastTransaction(bitcoinjTx).future(), new FutureCallback[BitcoinjTransaction] { - override def onFailure(t: Throwable): Unit = self ! BroadcastResult(tx, Failure(t)) - - override def onSuccess(v: BitcoinjTransaction): Unit = self ! BroadcastResult(tx, Success(true)) - }, context.dispatcher) - } - - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 1ffb2925d..8a2f24788 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -5,9 +5,8 @@ import java.nio.charset.StandardCharsets import akka.actor.{ActorRef, FSM, OneForOneStrategy, Props, Status, SupervisorStrategy} import akka.event.Logging.MDC import akka.pattern.pipe -import fr.acinq.bitcoin.Crypto.{PublicKey, ripemd160, sha256} +import fr.acinq.bitcoin.Crypto.{PublicKey, sha256} import fr.acinq.bitcoin._ -import fr.acinq.eclair.NodeParams.BITCOINJ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Helpers.{Closing, Funding} @@ -16,11 +15,10 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.{ChannelReestablish, _} -import org.bitcoinj.script.{Script => BitcoinjScript} import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -import scala.util.{Failure, Left, Random, Success, Try} +import scala.util.{Failure, Left, Success, Try} /** @@ -437,16 +435,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { case Event(FundingLocked(_, nextPerCommitmentPoint), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) => - if (d.commitments.announceChannel && nodeParams.watcherType == BITCOINJ && d.commitments.localParams.isFunder && System.getProperty("spvtest") != null) { - // bitcoinj-based watcher currently can't get the tx index in block (which is used to calculate the short id) - // instead, we rely on a hack by trusting the index the counterparty sends us - // but in testing when connecting to bitcoinj impl together we make the funder choose some random data - log.warning("using hardcoded short id for testing with bitcoinj!!!!!") - context.system.scheduler.scheduleOnce(5 seconds, self, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, Random.nextInt(100), Random.nextInt(100))) - } else { - // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) - blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) - } + // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) + blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId)) val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true) goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None)) @@ -759,11 +749,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // note: no need to persist their message, in case of disconnection they will resend it log.debug(s"received remote announcement signatures, delaying") context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) - if (nodeParams.watcherType == BITCOINJ) { - log.warning(s"HACK: since we cannot get the tx index with bitcoinj, we copy the value sent by remote") - val (blockHeight, txIndex, _) = fromShortId(remoteAnnSigs.shortChannelId) - self ! WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex) - } stay } @@ -1201,14 +1186,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } if (!d.buried) { - if (nodeParams.watcherType != BITCOINJ) { - // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been - // fired while we were in OFFLINE (if not, the operation is idempotent anyway) - blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) - } else { - // NB: in BITCOINJ mode we currently can't get the tx index in block (which is used to calculate the short id) - // instead, we rely on a hack by trusting the index the counterparty sends us) - } + // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been + // fired while we were in OFFLINE (if not, the operation is idempotent anyway) + blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) } else { // channel has been buried enough, should we (re)send our announcement sigs? d.channelAnnouncement match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index fcf87b0e5..722e5b9fd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -14,7 +14,6 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BasicBitcoinJsonRPCClient import fr.acinq.eclair.randomKey import fr.acinq.eclair.transactions.Scripts import grizzled.slf4j.Logging -import org.bitcoinj.script.{Script => BitcoinjScript} import org.json4s.JsonAST.JValue import org.json4s.{DefaultFormats, JString} import org.junit.runner.RunWith diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjSpec.scala deleted file mode 100644 index aef43ecce..000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoinj/BitcoinjSpec.scala +++ /dev/null @@ -1,217 +0,0 @@ -package fr.acinq.eclair.blockchain.bitcoinj - -import java.io.File -import java.net.InetSocketAddress -import java.nio.file.Files -import java.util.UUID - -import akka.actor.{Actor, ActorRef, ActorSystem, Props} -import akka.pattern.pipe -import akka.testkit.{TestKit, TestProbe} -import fr.acinq.bitcoin.{Satoshi, Script} -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient} -import fr.acinq.eclair.blockchain.{PublishAsap, WatchConfirmed, WatchEventConfirmed, WatchSpent} -import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT} -import fr.acinq.eclair.randomKey -import fr.acinq.eclair.transactions.Scripts -import grizzled.slf4j.Logging -import org.bitcoinj.script.{Script => BitcoinjScript} -import org.json4s.DefaultFormats -import org.json4s.JsonAST.JValue -import org.junit.Ignore -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner -import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} -import scala.sys.process.{Process, _} -import scala.util.Random - -@Ignore -@RunWith(classOf[JUnitRunner]) -class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging { - - val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/bitcoinj-${UUID.randomUUID().toString}" - logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - - val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind") - val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") - - var bitcoind: Process = null - var bitcoinrpcclient: BitcoinJsonRPCClient = null - var bitcoincli: ActorRef = null - - implicit val formats = DefaultFormats - - case class BitcoinReq(method: String, params: Any*) - - override def beforeAll(): Unit = { - Files.createDirectories(PATH_BITCOIND_DATADIR.toPath) - Files.copy(classOf[BitcoinjSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath) - - bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run() - bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332) - bitcoincli = system.actorOf(Props(new Actor { - override def receive: Receive = { - case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender - case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender - case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender - } - })) - } - - override def afterAll(): Unit = { - // gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging - logger.info(s"stopping bitcoind") - val sender = TestProbe() - sender.send(bitcoincli, BitcoinReq("stop")) - sender.expectMsgType[JValue] - //bitcoind.destroy() - // logger.warn(s"starting bitcoin-qt") - // val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath - // bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run() - } - - test("wait bitcoind ready") { - val sender = TestProbe() - logger.info(s"waiting for bitcoind to initialize...") - awaitCond({ - sender.send(bitcoincli, BitcoinReq("getnetworkinfo")) - sender.receiveOne(5 second).isInstanceOf[JValue] - }, max = 30 seconds, interval = 500 millis) - logger.info(s"generating initial blocks...") - sender.send(bitcoincli, BitcoinReq("generate", 500)) - sender.expectMsgType[JValue](30 seconds) - } - - test("bitcoinj wallet commit") { - val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj") - val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil) - bitcoinjKit.startAsync() - bitcoinjKit.awaitRunning() - - val sender = TestProbe() - val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet())) - - val address = Await.result(wallet.getFinalAddress, 10 seconds) - logger.info(s"sending funds to $address") - sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) - sender.expectMsgType[JValue](10 seconds) - awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 60 seconds, interval = 1 second) - - logger.info(s"generating blocks") - sender.send(bitcoincli, BitcoinReq("generate", 10)) - sender.expectMsgType[JValue](10 seconds) - - val fundingPubkeyScript1 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) - val result1 = Await.result(wallet.makeFundingTx(fundingPubkeyScript1, Satoshi(10000L), 20000), 10 seconds) - val fundingPubkeyScript2 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) - val result2 = Await.result(wallet.makeFundingTx(fundingPubkeyScript2, Satoshi(10000L), 20000), 10 seconds) - - assert(Await.result(wallet.commit(result1.fundingTx), 10 seconds) == true) - assert(Await.result(wallet.commit(result2.fundingTx), 10 seconds) == false) - - } - - /*def ticket() = { - val wallet: Wallet = ??? - - def makeTx(amount: Coin, script: BitcoinjScript): Transaction = { - val tx = new Transaction(wallet.getParams) - tx.addOutput(amount, script) - val req = SendRequest.forTx(tx) - wallet.completeTx(req) - tx - } - - val tx1 = makeTx(amount1, script1) - val tx2 = makeTx(amount2, script2) - - // everything is fine until here, and as expected tx1 and tx2 spend the same input - - wallet.maybeCommitTx(tx1) // returns true as expected - wallet.maybeCommitTx(tx2) // returns true! how come? - }*/ - - test("manual publish/watch") { - val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj") - val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil) - bitcoinjKit.startAsync() - bitcoinjKit.awaitRunning() - - val sender = TestProbe() - val watcher = system.actorOf(Props(new BitcoinjWatcher(bitcoinjKit)), name = "bitcoinj-watcher") - val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet())) - - val address = Await.result(wallet.getFinalAddress, 10 seconds) - logger.info(s"sending funds to $address") - sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) - sender.expectMsgType[JValue](10 seconds) - awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second) - - logger.info(s"generating blocks") - sender.send(bitcoincli, BitcoinReq("generate", 10)) - sender.expectMsgType[JValue](10 seconds) - - val listener = TestProbe() - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) - val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds) - assert(Await.result(wallet.commit(result.fundingTx), 10 seconds)) - watcher ! WatchSpent(listener.ref, result.fundingTx, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT) - watcher ! WatchConfirmed(listener.ref, result.fundingTx, 3, BITCOIN_FUNDING_DEPTHOK) - watcher ! PublishAsap(result.fundingTx) - - logger.info(s"waiting for confirmation of ${result.fundingTx.txid}") - val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds) - assert(event.event === BITCOIN_FUNDING_DEPTHOK) - } - - test("multiple publish/watch") { - val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj") - val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil) - bitcoinjKit.startAsync() - bitcoinjKit.awaitRunning() - - val sender = TestProbe() - val watcher = system.actorOf(Props(new BitcoinjWatcher(bitcoinjKit)), name = "bitcoinj-watcher") - val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet())) - - val address = Await.result(wallet.getFinalAddress, 10 seconds) - logger.info(s"sending funds to $address") - sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) - sender.expectMsgType[JValue](10 seconds) - awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second) - - def send() = { - val count = Random.nextInt(20) - val listeners = (0 to count).map { - case i => - val listener = TestProbe() - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) - val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds) - assert(Await.result(wallet.commit(result.fundingTx), 10 seconds)) - watcher ! WatchSpent(listener.ref, result.fundingTx, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT) - watcher ! WatchConfirmed(listener.ref, result.fundingTx, 3, BITCOIN_FUNDING_DEPTHOK) - watcher ! PublishAsap(result.fundingTx) - (result.fundingTx.txid, listener) - } - system.scheduler.scheduleOnce(2 seconds, new Runnable { - override def run() = { - logger.info(s"generating one block") - sender.send(bitcoincli, BitcoinReq("generate", 3)) - sender.expectMsgType[JValue](10 seconds) - } - }) - for ((txid, listener) <- listeners) { - logger.info(s"waiting for confirmation of $txid") - val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds) - assert(event.event === BITCOIN_FUNDING_DEPTHOK) - } - } - - for (i <- 0 to 10) send() - - } -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BasicBitcoinjIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BasicBitcoinjIntegrationSpec.scala deleted file mode 100644 index b4fbff838..000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BasicBitcoinjIntegrationSpec.scala +++ /dev/null @@ -1,661 +0,0 @@ -package fr.acinq.eclair.integration - -import java.io.{File, PrintWriter} -import java.nio.file.Files -import java.util.{Properties, UUID} - -import akka.actor.{Actor, ActorRef, ActorSystem, Props} -import akka.pattern.pipe -import akka.testkit.{TestKit, TestProbe} -import com.google.common.net.HostAndPort -import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script} -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient, ExtendedBitcoinClient} -import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjWallet -import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed} -import fr.acinq.eclair.channel.Register.Forward -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.Sphinx.ErrorPacket -import fr.acinq.eclair.io.Peer.Disconnect -import fr.acinq.eclair.io.{NodeURI, Peer} -import fr.acinq.eclair.payment.{State => _, _} -import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec} -import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Bitcoinj, Globals, Kit, Setup} -import grizzled.slf4j.Logging -import org.bitcoinj.core.Transaction -import org.json4s.DefaultFormats -import org.json4s.JsonAST.JValue -import org.junit.Ignore -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner -import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} - -import scala.collection.JavaConversions._ -import scala.concurrent.Await -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ -import scala.sys.process._ - -/** - * Created by PM on 15/03/2017. - */ -@RunWith(classOf[JUnitRunner]) -@Ignore -class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging { - - val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}" - logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - - System.setProperty("spvtest", "true") - - val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind") - val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") - - var bitcoind: Process = null - var bitcoinrpcclient: BitcoinJsonRPCClient = null - var bitcoincli: ActorRef = null - var nodes: Map[String, Kit] = Map() - - implicit val formats = DefaultFormats - - case class BitcoinReq(method: String, params: Any*) - - override def beforeAll(): Unit = { - Files.createDirectories(PATH_BITCOIND_DATADIR.toPath) - Files.copy(classOf[BasicIntegrationSpvSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath) - - bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run() - bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332) - bitcoincli = system.actorOf(Props(new Actor { - override def receive: Receive = { - case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender - case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender - case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender - case BitcoinReq(method, param1, param2, param3) => bitcoinrpcclient.invoke(method, param1, param2, param3) pipeTo sender - } - })) - } - - override def afterAll(): Unit = { - // gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging - logger.info(s"stopping bitcoind") - val sender = TestProbe() - sender.send(bitcoincli, BitcoinReq("stop")) - sender.expectMsgType[JValue] - //bitcoind.destroy() - nodes.foreach { - case (name, setup) => - logger.info(s"stopping node $name") - setup.system.terminate() - } - // logger.warn(s"starting bitcoin-qt") - // val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath - // bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run() - } - - test("wait bitcoind ready") { - val sender = TestProbe() - logger.info(s"waiting for bitcoind to initialize...") - awaitCond({ - sender.send(bitcoincli, BitcoinReq("getnetworkinfo")) - sender.receiveOne(5 second).isInstanceOf[JValue] - }, max = 30 seconds, interval = 500 millis) - logger.info(s"generating initial blocks...") - sender.send(bitcoincli, BitcoinReq("generate", 500)) - sender.expectMsgType[JValue](10 seconds) - } - - def instantiateEclairNode(name: String, config: Config) = { - val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name") - datadir.mkdirs() - new PrintWriter(new File(datadir, "eclair.conf")) { - write(config.root().render()); - close - } - val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name")) - val kit = Await.result(setup.bootstrap, 10 seconds) - setup.bitcoin.asInstanceOf[Bitcoinj].bitcoinjKit.awaitRunning() - nodes = nodes + (name -> kit) - } - - def javaProps(props: Seq[(String, String)]) = { - val properties = new Properties() - props.foreach(p => properties.setProperty(p._1, p._2)) - properties - } - - test("starting eclair nodes") { - import collection.JavaConversions._ - val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> true, "eclair.chain" -> "regtest", "eclair.bitcoinj.static-peers.0.host" -> "localhost", "eclair.bitcoinj.static-peers.0.port" -> 28333, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false, "eclair.delay-blocks" -> 6)) - //instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 29730, "eclair.api.port" -> 28080)).withFallback(commonConfig)) - //instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 29732, "eclair.api.port" -> 28082)).withFallback(commonConfig)) - //instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.server.port" -> 29733, "eclair.api.port" -> 28083)).withFallback(commonConfig)) - //instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig)) - //instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) // NB: eclair.payment-handler = noop allows us to manually fulfill htlcs - //instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) - instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) - instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.server.port" -> 29738, "eclair.api.port" -> 28088, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) - } - - def sendFunds(node: Kit) = { - val sender = TestProbe() - val address = Await.result(node.wallet.getFinalAddress, 10 seconds) - logger.info(s"sending funds to $address") - sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) - sender.expectMsgType[JValue](10 seconds) - awaitCond({ - node.wallet.getBalance.pipeTo(sender.ref) - sender.expectMsgType[Satoshi] > Satoshi(0) - }, max = 30 seconds, interval = 1 second) - } - - test("fund eclair wallets") { - //sendFunds(nodes("A")) - //sendFunds(nodes("B")) - sendFunds(nodes("C")) - //sendFunds(nodes("D")) - //sendFunds(nodes("E")) - } - - def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = { - val eventListener1 = TestProbe() - val eventListener2 = TestProbe() - node1.system.eventStream.subscribe(eventListener1.ref, classOf[ChannelStateChanged]) - node2.system.eventStream.subscribe(eventListener2.ref, classOf[ChannelStateChanged]) - val sender = TestProbe() - val address = node2.nodeParams.publicAddresses.head - sender.send(node1.switchboard, Peer.Connect(NodeURI( - nodeId = node2.nodeParams.privateKey.publicKey, - address = HostAndPort.fromParts(address.getHostString, address.getPort)))) - sender.expectMsgAnyOf(10 seconds, "connected", "already connected") - sender.send(node1.switchboard, Peer.OpenChannel( - remoteNodeId = node2.nodeParams.privateKey.publicKey, - fundingSatoshis = Satoshi(fundingSatoshis), - pushMsat = MilliSatoshi(pushMsat), - channelFlags = None)) - sender.expectMsgAnyOf(10 seconds, "channel created") - - awaitCond(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds) - awaitCond(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds) - } - - test("connect nodes") { - // - // A ---- B ---- C ---- D - // | / \ - // --E--' F{1,2,3,4} - // - - val sender = TestProbe() - val eventListener = TestProbe() - nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) - - //connect(nodes("A"), nodes("B"), 10000000, 0) - //connect(nodes("B"), nodes("C"), 2000000, 0) - //connect(nodes("C"), nodes("D"), 5000000, 0) - //connect(nodes("B"), nodes("E"), 5000000, 0) - //connect(nodes("E"), nodes("C"), 5000000, 0) - //connect(nodes("C"), nodes("F1"), 5000000, 0) - //connect(nodes("C"), nodes("F2"), 5000000, 0) - connect(nodes("C"), nodes("F3"), 5000000, 0) - connect(nodes("C"), nodes("F4"), 5000000, 0) - - // a channel has two endpoints - val channelEndpointsCount = nodes.values.foldLeft(0) { - case (sum, setup) => - sender.send(setup.register, 'channels) - val channels = sender.expectMsgType[Map[BinaryData, ActorRef]] - sum + channels.size - } - - // we make sure all channels have set up their WatchConfirmed for the funding tx - awaitCond({ - nodes.values.foldLeft(Set.empty[Watch]) { - case (watches, setup) => - sender.send(setup.watcher, 'watches) - watches ++ sender.expectMsgType[Set[Watch]] - }.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount - }, max = 10 seconds, interval = 1 second) - - // confirming the funding tx - sender.send(bitcoincli, BitcoinReq("generate", 2)) - sender.expectMsgType[JValue](10 seconds) - - within(60 seconds) { - var count = 0 - while (count < channelEndpointsCount) { - if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == NORMAL) count = count + 1 - } - } - } - - def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int) = { - val sender = TestProbe() - subset.foreach { - case (_, setup) => - awaitCond({ - sender.send(setup.router, 'nodes) - sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes - }, max = 60 seconds, interval = 1 second) - awaitCond({ - sender.send(setup.router, 'channels) - sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels - }, max = 60 seconds, interval = 1 second) - awaitCond({ - sender.send(setup.router, 'updates) - sender.expectMsgType[Iterable[ChannelUpdate]].size == updates - }, max = 60 seconds, interval = 1 second) - } - } - - test("wait for network announcements") { - val sender = TestProbe() - // generating more blocks so that all funding txes are buried under at least 6 blocks - sender.send(bitcoincli, BitcoinReq("generate", 4)) - sender.expectMsgType[JValue] - awaitAnnouncements(nodes, 3, 2, 4) - } - - ignore("send an HTLC A->D") { - val sender = TestProbe() - val amountMsat = MilliSatoshi(4200000) - // first we retrieve a payment hash from D - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - // then we make the actual payment - sender.send(nodes("A").paymentInitiator, - SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)) - sender.expectMsgType[PaymentSucceeded] - } - - ignore("send an HTLC A->D with an invalid expiry delta for C") { - val sender = TestProbe() - // to simulate this, we will update C's relay params - // first we find out the short channel id for channel C-D, easiest way is to ask D's register which has only one channel - sender.send(nodes("D").register, 'shortIds) - val shortIdCD = sender.expectMsgType[Map[Long, BinaryData]].keys.head - val channelUpdateCD = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.blockId, nodes("C").nodeParams.privateKey, nodes("D").nodeParams.privateKey.publicKey, shortIdCD, nodes("D").nodeParams.expiryDeltaBlocks + 1, nodes("D").nodeParams.htlcMinimumMsat, nodes("D").nodeParams.feeBaseMsat, nodes("D").nodeParams.feeProportionalMillionth) - sender.send(nodes("C").relayer, channelUpdateCD) - // first we retrieve a payment hash from D - val amountMsat = MilliSatoshi(4200000) - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - // then we make the actual payment - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey) - sender.send(nodes("A").paymentInitiator, sendReq) - // A will receive an error from C that include the updated channel update, then will retry the payment - sender.expectMsgType[PaymentSucceeded](5 seconds) - // in the meantime, the router will have updated its state - awaitCond({ - sender.send(nodes("A").router, 'updates) - sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateCD) - }, max = 20 seconds, interval = 1 second) - // finally we retry the same payment, this time successfully - } - - ignore("send an HTLC A->D with an amount greater than capacity of C-D") { - val sender = TestProbe() - // first we retrieve a payment hash from D - val amountMsat = MilliSatoshi(300000000L) - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - // then we make the payment (C-D has a smaller capacity than A-B and B-C) - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey) - sender.send(nodes("A").paymentInitiator, sendReq) - // A will first receive an error from C, then retry and route around C: A->B->E->C->D - sender.expectMsgType[PaymentSucceeded](5 seconds) - } - - ignore("send an HTLC A->D with an unknown payment hash") { - val sender = TestProbe() - val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey) - sender.send(nodes("A").paymentInitiator, pr) - - // A will first receive an error from C, then retry and route around C: A->B->E->C->D - val failed = sender.expectMsgType[PaymentFailed] - assert(failed.paymentHash === pr.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, UnknownPaymentHash)) - } - - ignore("send an HTLC A->D with a lower amount than requested") { - val sender = TestProbe() - // first we retrieve a payment hash from D for 2 mBTC - val amountMsat = MilliSatoshi(200000000L) - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - - // A send payment of only 1 mBTC - val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey) - sender.send(nodes("A").paymentInitiator, sendReq) - - // A will first receive an IncorrectPaymentAmount error from D - val failed = sender.expectMsgType[PaymentFailed] - assert(failed.paymentHash === pr.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount)) - } - - ignore("send an HTLC A->D with too much overpayment") { - val sender = TestProbe() - // first we retrieve a payment hash from D for 2 mBTC - val amountMsat = MilliSatoshi(200000000L) - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - - // A send payment of 6 mBTC - val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey) - sender.send(nodes("A").paymentInitiator, sendReq) - - // A will first receive an IncorrectPaymentAmount error from D - val failed = sender.expectMsgType[PaymentFailed] - assert(failed.paymentHash === pr.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount)) - } - - ignore("send an HTLC A->D with a reasonable overpayment") { - val sender = TestProbe() - // first we retrieve a payment hash from D for 2 mBTC - val amountMsat = MilliSatoshi(200000000L) - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) - val pr = sender.expectMsgType[PaymentRequest] - - // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey) - sender.send(nodes("A").paymentInitiator, sendReq) - sender.expectMsgType[PaymentSucceeded] - } - - /** - * We currently use p2pkh script Helpers.getFinalScriptPubKey - * - * @param scriptPubKey - * @return - */ - def scriptPubKeyToAddress(scriptPubKey: BinaryData) = Script.parse(scriptPubKey) match { - case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil => - Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) - case OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUAL :: Nil => - Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, pubKeyHash) - case _ => ??? - } - - def incomingTxes(node: Kit) = { - val sender = TestProbe() - (for { - w <- nodes("F1").wallet.asInstanceOf[BitcoinjWallet].fWallet - txes = w.getWalletTransactions - incomingTxes = txes.toSet.filter(tx => tx.getTransaction.getValueSentToMe(w).longValue() > 0) - } yield incomingTxes).pipeTo(sender.ref) - sender.expectMsgType[Set[Transaction]] - } - - ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit)") { - val sender = TestProbe() - // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) - // we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test - val initialTxesC = incomingTxes(nodes("C")) - val initialTxesF1 = incomingTxes(nodes("F1")) - // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs - val htlcReceiver = TestProbe() - // we register this probe as the final payment handler - nodes("F1").paymentHandler ! htlcReceiver.ref - val preimage: BinaryData = "42" * 32 - val paymentHash = Crypto.sha256(preimage) - // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.privateKey.publicKey, maxAttempts = 1) - val paymentSender = TestProbe() - paymentSender.send(nodes("A").paymentInitiator, paymentReq) - // F gets the htlc - val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] - // we then kill the connection between C and F - sender.send(nodes("F1").switchboard, 'peers) - val peers = sender.expectMsgType[Map[PublicKey, ActorRef]] - peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect - // we then wait for F to be in disconnected state - awaitCond({ - sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE)) - sender.expectMsgType[State] == OFFLINE - }, max = 20 seconds, interval = 1 second) - // we then have C unilateral close the channel (which will make F redeem the htlc onchain) - sender.send(nodes("C").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT)) - // we then wait for F to detect the unilateral close and go to CLOSING state - awaitCond({ - sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE)) - sender.expectMsgType[State] == CLOSING - }, max = 20 seconds, interval = 1 second) - // we then fulfill the htlc, which will make F redeem it on-chain - sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) - // we then generate one block so that the htlc success tx gets written to the blockchain - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - // C will extract the preimage from the blockchain and fulfill the payment upstream - paymentSender.expectMsgType[PaymentSucceeded](30 seconds) - // at this point F should have received the on-chain tx corresponding to the redeemed htlc - awaitCond({ - incomingTxes(nodes("F1")).size - initialTxesF1.size == 1 - }, max = 30 seconds, interval = 1 second) - // we then generate enough blocks so that C gets its main delayed output - for (i <- 0 until 7) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - } - // and C will have its main output - awaitCond({ - incomingTxes(nodes("C")).size - initialTxesC.size == 1 - }, max = 30 seconds, interval = 1 second) - // TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16) - } - - ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") { - val sender = TestProbe() - // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - sender.send(bitcoincli, BitcoinReq("getbestblockhash")) - val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) - // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs - val htlcReceiver = TestProbe() - // we register this probe as the final payment handler - nodes("F2").paymentHandler ! htlcReceiver.ref - val preimage: BinaryData = "42" * 32 - val paymentHash = Crypto.sha256(preimage) - // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.privateKey.publicKey, maxAttempts = 1) - val paymentSender = TestProbe() - paymentSender.send(nodes("A").paymentInitiator, paymentReq) - // F gets the htlc - val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] - // now that we have the channel id, we retrieve channels default final addresses - sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) - val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey - sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) - val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey - // we then kill the connection between C and F - sender.send(nodes("F2").switchboard, 'peers) - val peers = sender.expectMsgType[Map[PublicKey, ActorRef]] - peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect - // we then wait for F to be in disconnected state - awaitCond({ - sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATE)) - sender.expectMsgType[State] == OFFLINE - }, max = 20 seconds, interval = 1 second) - // then we have F unilateral close the channel - sender.send(nodes("F2").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT)) - // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) - sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) - // we then generate one block so that the htlc success tx gets written to the blockchain - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - // C will extract the preimage from the blockchain and fulfill the payment upstream - paymentSender.expectMsgType[PaymentSucceeded](30 seconds) - // at this point F should have 1 recv transactions: the redeemed htlc - // we then generate enough blocks so that F gets its htlc-success delayed output - for (i <- 0 until 7) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - } - val ext = new ExtendedBitcoinClient(bitcoinrpcclient) - awaitCond({ - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - //ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref) - val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_)) - // at this point F should have 1 recv transactions: the redeemed htlc and C will have its main output - txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 1 && - txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 1 - }, max = 30 seconds, interval = 1 second) - // TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14) - } - - test("propagate a failure upstream when a downstream htlc times out (local commit)") { - val sender = TestProbe() - // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - sender.send(bitcoincli, BitcoinReq("getbestblockhash")) - val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) - // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs - val htlcReceiver = TestProbe() - // we register this probe as the final payment handler - nodes("F3").paymentHandler ! htlcReceiver.ref - val preimage: BinaryData = "42" * 32 - val paymentHash = Crypto.sha256(preimage) - // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.privateKey.publicKey, maxAttempts = 1) - val paymentSender = TestProbe() - paymentSender.send(nodes("C").paymentInitiator, paymentReq) - // F gets the htlc - val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] - // now that we have the channel id, we retrieve channels default final addresses - sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) - val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey - sender.send(nodes("F3").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) - val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey - // we then generate enough blocks to make the htlc timeout - for (i <- 0 until 11) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - } - // this will fail the htlc - //val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds) - //assert(failed.paymentHash === paymentHash) - //assert(failed.failures.size === 1) - //assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure)) - // we then generate enough blocks to confirm all delayed transactions - for (i <- 0 until 7) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - } - val ext = new ExtendedBitcoinClient(bitcoinrpcclient) - awaitCond({ - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - //ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref) - val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_)) - // at this point C should have 2 recv transactions: its main output and the htlc timeout - txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 && - txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2 - }, max = 30 seconds, interval = 1 second) - // TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12) - } - - test("propagate a failure upstream when a downstream htlc times out (remote commit)") { - val sender = TestProbe() - // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - sender.send(bitcoincli, BitcoinReq("getbestblockhash")) - val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) - // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs - val htlcReceiver = TestProbe() - // we register this probe as the final payment handler - nodes("F4").paymentHandler ! htlcReceiver.ref - val preimage: BinaryData = "42" * 32 - val paymentHash = Crypto.sha256(preimage) - // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.privateKey.publicKey, maxAttempts = 1) - val paymentSender = TestProbe() - paymentSender.send(nodes("C").paymentInitiator, paymentReq) - // F gets the htlc - val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] - // now that we have the channel id, we retrieve channels default final addresses - sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) - val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey - sender.send(nodes("F4").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) - val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey - // then we ask F to unilaterally close the channel - sender.send(nodes("F4").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT)) - // we then generate enough blocks to make the htlc timeout - for (i <- 0 until 11) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - } - // this will fail the htlc - //val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds) - //assert(failed.paymentHash === paymentHash) - //assert(failed.failures.size === 1) - //assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure)) - // we then generate enough blocks to confirm all delayed transactions - for (i <- 0 until 7) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - } - val ext = new ExtendedBitcoinClient(bitcoinrpcclient) - awaitCond({ - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - //ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref) - val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_)) - // at this point C should have 2 recv transactions: its main output and the htlc timeout - txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 && - txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2 - }, max = 30 seconds, interval = 1 second) - // TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10) - } - - ignore("generate and validate lots of channels") { - implicit val extendedClient = new ExtendedBitcoinClient(bitcoinrpcclient) - // we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random - logger.info(s"generating fake channels") - val sender = TestProbe() - val channels = for (i <- 0 until 242) yield { - // let's generate a block every 10 txs so that we can compute short ids - if (i % 10 == 0) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - } - AnnouncementsBatchValidationSpec.simulateChannel - } - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - logger.info(s"simulated ${channels.size} channels") - // then we make the announcements - val announcements = channels.map(c => AnnouncementsBatchValidationSpec.makeChannelAnnouncement(c)) - announcements.foreach(ann => nodes("A").router ! ann) - awaitCond({ - sender.send(nodes("D").router, 'channels) - sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared - }, max = 120 seconds, interval = 1 second) - } - - -} diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala index cda383f13..247861b14 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala @@ -26,7 +26,7 @@ import javafx.util.{Callback, Duration} import com.google.common.net.HostAndPort import fr.acinq.bitcoin.{MilliSatoshi, Satoshi} -import fr.acinq.eclair.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM} +import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} import fr.acinq.eclair.Setup import fr.acinq.eclair.gui.stages._ import fr.acinq.eclair.gui.utils.{CoinUtils, ContextMenuUtils, CopyAction} @@ -330,7 +330,6 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext val wallet = setup.nodeParams.watcherType match { case BITCOIND => "Bitcoin-core" case ELECTRUM => "Electrum" - case BITCOINJ => "BitcoinJ" } bitcoinWallet.setText(wallet) bitcoinChain.setText(s"${setup.chain.toUpperCase()}") diff --git a/pom.xml b/pom.xml index dcad66c1e..86c44fd14 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 2.11 2.4.18 0.9.14 - 0.15-rc4 + 24.0-android