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 07b28d0e6..6a8c827ae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -131,7 +131,7 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act } val wallet = bitcoin match { - case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher) + case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient) case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet())) case Electrum(electrumClient) => val electrumSeedPath = new File(datadir, "electrum_seed.dat") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index 8c7a4b4e0..1fbdc892e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -1,55 +1,32 @@ package fr.acinq.eclair.blockchain.bitcoind -import akka.actor.{Actor, ActorRef, ActorSystem, Props} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Base58Check, BinaryData, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import akka.actor.ActorSystem +import fr.acinq.bitcoin.{BinaryData, OutPoint, Satoshi, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError} -import fr.acinq.eclair.channel.{BITCOIN_OUTPUT_SPENT, BITCOIN_TX_CONFIRMED} import fr.acinq.eclair.transactions.Transactions import grizzled.slf4j.Logging -import org.json4s.JsonAST.{JBool, JDouble, JInt, JString} +import org.json4s.JsonAST._ -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future, Promise} +import scala.concurrent.{ExecutionContext, Future} /** - * Due to bitcoin-core wallet not fully supporting segwit txes yet, our current scheme is: - * utxos <- parent-tx <- funding-tx - * - * With: - * - utxos may be non-segwit - * - parent-tx pays to a p2wpkh segwit output - * - funding-tx is a segwit tx - * * Created by PM on 06/07/2017. */ -class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging { +class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging { - override def getBalance: Future[Satoshi] = ??? + import BitcoinCoreWallet._ - override def getFinalAddress: Future[String] = rpcClient.invoke("getnewaddress").map(json => { - val JString(address) = json - address - }) - - case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double) - - case class SignTransactionResponse(tx: Transaction, complete: Boolean) - - case class MakeFundingTxResponseWithParent(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey) - - def fundTransaction(hex: String, lockUnspents: Boolean): Future[FundTransactionResponse] = { - rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(lockUnspents)).map(json => { + def fundTransaction(hex: String, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = { + rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(changeAddress, lockUnspents)).map(json => { val JString(hex) = json \ "hex" val JInt(changepos) = json \ "changepos" val JDouble(fee) = json \ "fee" - FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee) + FundTransactionResponse(Transaction.read(hex), changepos.intValue(), (fee * 10e8).toLong) }) } - def fundTransaction(tx: Transaction, lockUnspents: Boolean): Future[FundTransactionResponse] = - fundTransaction(Transaction.write(tx).toString(), lockUnspents) + def fundTransaction(tx: Transaction, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toString(), changeAddress, lockUnspents) def signTransaction(hex: String): Future[SignTransactionResponse] = rpcClient.invoke("signrawtransaction", hex).map(json => { @@ -58,157 +35,66 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(impl SignTransactionResponse(Transaction.read(hex), complete) }) - def signTransaction(tx: Transaction): Future[SignTransactionResponse] = - signTransaction(Transaction.write(tx).toString()) + def signTransaction(tx: Transaction): Future[SignTransactionResponse] = signTransaction(Transaction.write(tx).toString()) - def getTransaction(txid: BinaryData): Future[Transaction] = { - rpcClient.invoke("getrawtransaction", txid.toString()).map(json => { - val JString(hex) = json - Transaction.read(hex) - }) - } + def getTransaction(txid: BinaryData): Future[Transaction] = rpcClient.invoke("getrawtransaction", txid.toString()) collect { case JString(hex) => Transaction.read(hex) } - def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = - publishTransaction(Transaction.write(tx).toString()) + def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = publishTransaction(Transaction.write(tx).toString()) - def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = - rpcClient.invoke("sendrawtransaction", hex) collect { - case JString(txid) => txid - } + def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = rpcClient.invoke("sendrawtransaction", hex) collect { case JString(txid) => txid } - /** - * - * @param fundingTxResponse a funding tx response - * @return an updated funding tx response that is properly sign - */ - def sign(fundingTxResponse: MakeFundingTxResponseWithParent): MakeFundingTxResponseWithParent = { - // find the output that we are spending from - val utxo = fundingTxResponse.parentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt) + def unlockOutpoint(outPoints: List[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = rpcClient.invoke("lockunspent", true, outPoints.map(outPoint => Utxo(outPoint.txid.toString, outPoint.index))) collect { case JBool(result) => result } - val pub = fundingTxResponse.priv.publicKey - val pubKeyScript = Script.pay2pkh(pub) - val sig = Transaction.signInput(fundingTxResponse.fundingTx, 0, pubKeyScript, SIGHASH_ALL, utxo.amount, SigVersion.SIGVERSION_WITNESS_V0, fundingTxResponse.priv) - val witness = ScriptWitness(Seq(sig, pub.toBin)) - val fundingTx1 = fundingTxResponse.fundingTx.updateSigScript(0, OP_PUSHDATA(Script.write(Script.pay2wpkh(pub))) :: Nil).updateWitness(0, witness) - Transaction.correctlySpends(fundingTx1, fundingTxResponse.parentTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - fundingTxResponse.copy(fundingTx = fundingTx1) - } + override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDouble(balance) => Satoshi((balance * 10e8).toLong) } - /** - * - * @param fundingTxResponse funding transaction response, which includes a funding tx, its parent, and the private key - * that we need to re-sign the funding - * @param newParentTx new parent tx - * @return an updated funding transaction response where the funding tx now spends from newParentTx - */ - def replaceParent(fundingTxResponse: MakeFundingTxResponseWithParent, newParentTx: Transaction): MakeFundingTxResponseWithParent = { - // find the output that we are spending from - val utxo = newParentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt) + override def getFinalAddress: Future[String] = for { + JString(address) <- rpcClient.invoke("getnewaddress") + // we want bitcoind to only use segwit addresses to avoid malleability issues + JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address) + } yield segwitAddress - // check that it matches what we expect, which is a P2WPKH output to our public key - require(utxo.publicKeyScript == Script.write(Script.pay2sh(Script.pay2wpkh(fundingTxResponse.priv.publicKey)))) - - // update our tx input we the hash of the new parent - val input = fundingTxResponse.fundingTx.txIn(0) - val input1 = input.copy(outPoint = input.outPoint.copy(hash = newParentTx.hash)) - val unsignedFundingTx = fundingTxResponse.fundingTx.copy(txIn = Seq(input1)) - - // and re-sign it - sign(MakeFundingTxResponseWithParent(newParentTx, unsignedFundingTx, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.priv)) - } - - def makeParentAndFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponseWithParent] = + override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = for { - // ask for a new address and the corresponding private key - JString(address) <- rpcClient.invoke("getnewaddress") - JString(wif) <- rpcClient.invoke("dumpprivkey", address) - JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address) - (prefix, raw) = Base58Check.decode(wif) - priv = PrivateKey(raw, compressed = true) - pub = priv.publicKey - // create a tx that sends money to a P2SH(WPKH) output that matches our private key - parentFee = Satoshi(250 * 2 * 2 * feeRatePerKw / 1024) - partialParentTx = Transaction( - version = 2, - txIn = Nil, - txOut = TxOut(amount + parentFee, Script.pay2sh(Script.pay2wpkh(pub))) :: Nil, - lockTime = 0L) - FundTransactionResponse(unsignedParentTx, _, _) <- fundTransaction(partialParentTx, lockUnspents = true) - // this is the first tx that we will publish, a standard tx which send money to our p2wpkh address - SignTransactionResponse(parentTx, true) <- signTransaction(unsignedParentTx) - // now we create the funding tx + // we create a new segwit change address (we don't want bitcoin core to use regular malleable outputs) + JString(changeAddress) <- rpcClient.invoke("getnewaddress") + JString(segwitChangeAddress) <- rpcClient.invoke("addwitnessaddress", changeAddress) + _ = logger.debug(s"using segwitChangeAddress=$segwitChangeAddress") + // partial funding tx partialFundingTx = Transaction( version = 2, txIn = Seq.empty[TxIn], txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0) - // and update it to spend from our segwit tx - pos = Transactions.findPubKeyScriptIndex(parentTx, Script.pay2sh(Script.pay2wpkh(pub))) - unsignedFundingTx = partialFundingTx.copy(txIn = TxIn(OutPoint(parentTx, pos), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil) - } yield sign(MakeFundingTxResponseWithParent(parentTx, unsignedFundingTx, 0, priv)) - - /** - * This is a workaround for malleability - * - * @param pubkeyScript - * @param amount - * @param feeRatePerKw - * @return - */ - override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = { - val promise = Promise[MakeFundingTxResponse]() - (for { - fundingTxResponse@MakeFundingTxResponseWithParent(parentTx, _, _, _) <- makeParentAndFundingTx(pubkeyScript, amount, feeRatePerKw) - input0 = parentTx.txIn.head - parentOfParentTx <- getTransaction(input0.outPoint.txid) - _ = logger.debug(s"built parentTxid=${parentTx.txid}, initializing temporary actor") - tempActor = system.actorOf(Props(new Actor { - override def receive: Receive = { - case WatchEventSpent(BITCOIN_OUTPUT_SPENT, spendingTx) => - if (parentTx.txid != spendingTx.txid) { - // an input of our parent tx was spent by a tx that we're not aware of (i.e. a malleated version of our parent tx) - // set a new watch; if it is confirmed, we'll use it as the new parent for our funding tx - logger.warn(s"parent tx has been malleated: originalParentTxid=${parentTx.txid} malleated=${spendingTx.txid}") - } - watcher ! WatchConfirmed(self, spendingTx.txid, spendingTx.txOut(0).publicKeyScript, minDepth = 1, BITCOIN_TX_CONFIRMED(spendingTx)) - - case WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _) => - // a potential parent for our funding tx has been confirmed, let's update our funding tx - val finalFundingTx = replaceParent(fundingTxResponse, tx) - promise.success(MakeFundingTxResponse(finalFundingTx.fundingTx, finalFundingTx.fundingTxOutputIndex)) - } - })) - // we watch the first input of the parent tx, so that we can detect when it is spent by a malleated avatar - _ = watcher ! WatchSpent(tempActor, input0.outPoint.txid, input0.outPoint.index.toInt, parentOfParentTx.txOut(input0.outPoint.index.toInt).publicKeyScript, BITCOIN_OUTPUT_SPENT) - // and we publish the parent tx - _ = logger.info(s"publishing parent tx: txid=${parentTx.txid} tx=${Transaction.write(parentTx)}") - // we use a small delay so that we are sure Publish doesn't race with WatchSpent (which is ok but generates unnecessary warnings) - _ = system.scheduler.scheduleOnce(100 milliseconds, watcher, PublishAsap(parentTx)) - } yield {}) onFailure { - case t: Throwable => promise.failure(t) - } - promise.future - } + // we ask bitcoin core to add inputs to the funding tx, and use the specified change address + FundTransactionResponse(unsignedFundingTx, changepos, fee) <- fundTransaction(partialFundingTx, segwitChangeAddress, lockUnspents = true) + // now let's sign the funding tx + SignTransactionResponse(fundingTx, _) <- signTransaction(unsignedFundingTx) + // there will probably be a change output, so we need to find which output is ours + outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript) + _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee") + } yield MakeFundingTxResponse(fundingTx, outputIndex) override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx) .map(_ => true) // if bitcoind says OK, then we consider the tx succesfully published - .recoverWith { case JsonRPCError(_) => getTransaction(tx.txid).map(_ => true).recover { case _ => false } } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published + .recoverWith { case JsonRPCError(e) => + logger.warn(s"txid=${tx.txid} error=$e") + getTransaction(tx.txid).map(_ => true).recover { case _ => false } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published + } .recover { case _ => true } // in all other cases we consider that the tx has been published + override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoint(tx.txIn.map(_.outPoint).toList) // we unlock all utxos used by the tx - /** - * We currently only put a lock on the parent tx inputs, and we publish the parent tx immediately so there is nothing - * to do here. - * - * @param tx - * @return - */ - override def rollback(tx: Transaction): Future[Boolean] = Future.successful(true) } object BitcoinCoreWallet { - case class Options(lockUnspents: Boolean) + // @formatter:off + case class Options(changeAddress: String, lockUnspents: Boolean) + case class Utxo(txid: String, vout: Long) + case class FundTransactionResponse(tx: Transaction, changepos: Int, feeSatoshis: Long) + case class SignTransactionResponse(tx: Transaction, complete: Boolean) + // @formatter:on + } \ No newline at end of file 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 new file mode 100644 index 000000000..f14028f37 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -0,0 +1,137 @@ +package fr.acinq.eclair.blockchain.bitcoind + +import java.io.File +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 com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.{MilliBtc, Satoshi, Script, Transaction} +import fr.acinq.eclair.blockchain._ +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient +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 +import org.scalatest.junit.JUnitRunner +import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.sys.process.{Process, _} + +@RunWith(classOf[JUnitRunner]) +class BitcoinCoreWalletSpec 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[BitcoinCoreWalletSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath) + + bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run() + bitcoinrpcclient = new BitcoinJsonRPCClient(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("create/commit/rollback funding txes") { + import collection.JavaConversions._ + val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "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)) + val config = ConfigFactory.load(commonConfig).getConfig("eclair") + val bitcoinClient = new BitcoinJsonRPCClient( + user = config.getString("bitcoind.rpcuser"), + password = config.getString("bitcoind.rpcpassword"), + host = config.getString("bitcoind.host"), + port = config.getInt("bitcoind.rpcport")) + val wallet = new BitcoinCoreWallet(bitcoinClient) + + val sender = TestProbe() + + wallet.getBalance.pipeTo(sender.ref) + assert(sender.expectMsgType[Satoshi] > Satoshi(0)) + + wallet.getFinalAddress.pipeTo(sender.ref) + assert(sender.expectMsgType[String].startsWith("2")) + + val fundingTxes = for (i <- 0 to 3) yield { + val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) + wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref) + val MakeFundingTxResponse(fundingTx, _) = sender.expectMsgType[MakeFundingTxResponse] + fundingTx + } + + sender.send(bitcoincli, BitcoinReq("listlockunspent")) + assert(sender.expectMsgType[JValue](10 seconds).children.size === 4) + + wallet.commit(fundingTxes(0)).pipeTo(sender.ref) + assert(sender.expectMsgType[Boolean]) + + wallet.rollback(fundingTxes(1)).pipeTo(sender.ref) + assert(sender.expectMsgType[Boolean]) + + wallet.commit(fundingTxes(2)).pipeTo(sender.ref) + assert(sender.expectMsgType[Boolean]) + + wallet.rollback(fundingTxes(3)).pipeTo(sender.ref) + assert(sender.expectMsgType[Boolean]) + + sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(0).txid.toString())) + assert(sender.expectMsgType[JString](10 seconds).s === Transaction.write(fundingTxes(0)).toString()) + + sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString())) + assert(sender.expectMsgType[JString](10 seconds).s === Transaction.write(fundingTxes(2)).toString()) + + // NB: bitcoin core doesn't clear the locks when a tx is published + sender.send(bitcoincli, BitcoinReq("listlockunspent")) + assert(sender.expectMsgType[JValue](10 seconds).children.size === 2) + + } + +} \ No newline at end of file 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 index 4a9280096..0e510ce12 100644 --- 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 @@ -18,6 +18,7 @@ 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} @@ -28,6 +29,7 @@ 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 { @@ -84,7 +86,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B sender.expectMsgType[JValue](30 seconds) } - ignore("bitcoinj wallet commit") { + 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() @@ -133,7 +135,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B wallet.maybeCommitTx(tx2) // returns true! how come? }*/ - ignore("manual publish/watch") { + 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() @@ -166,7 +168,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B assert(event.event === BITCOIN_FUNDING_DEPTHOK) } - ignore("multiple publish/watch") { + 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() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 7d4a39b24..a4c87f466 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -9,7 +9,7 @@ import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} 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_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction} +import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed} import fr.acinq.eclair.channel.Register.Forward @@ -131,31 +131,25 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit } 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() sender.send(node1.switchboard, NewConnection( remoteNodeId = node2.nodeParams.privateKey.publicKey, address = node2.nodeParams.publicAddresses.head, newChannel_opt = Some(NewChannel(Satoshi(fundingSatoshis), MilliSatoshi(pushMsat), None)))) sender.expectMsgAnyOf(10 seconds, "connected", s"already connected to nodeId=${node2.nodeParams.privateKey.publicKey.toBin}") - // funder transitions - assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_ACCEPT_CHANNEL) - assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_INTERNAL) - // fundee transitions - assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_OPEN_CHANNEL) - assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CREATED) } test("connect nodes") { // // A ---- B ---- C ---- D // | / \ - // --E--' F{1,2,3,4} + // --E--' F{1,2,3,4,5} // + 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) @@ -167,51 +161,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit connect(nodes("C"), nodes("F4"), 5000000, 0) connect(nodes("C"), nodes("F5"), 5000000, 0) - val sender = TestProbe() - val eventListener = TestProbe() - nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) + val numberOfChannels = 10 + val channelEndpointsCount = 2 * numberOfChannels - // 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 - } - - // each funder sets up a WatchConfirmed on the parent tx, we need to make sure it has been received by the watcher - var watches1 = Set.empty[Watch] + // we make sure all channels have set up their WatchConfirmed for the funding tx awaitCond({ - watches1 = nodes.values.foldLeft(Set.empty[Watch]) { + val watches = nodes.values.foldLeft(Set.empty[Watch]) { case (watches, setup) => sender.send(setup.watcher, 'watches) watches ++ sender.expectMsgType[Set[Watch]] } - watches1.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount / 2 + watches.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount }, max = 10 seconds, interval = 1 second) - // confirming the parent tx of the funding - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) - - within(30 seconds) { - var count = 0 - while (count < channelEndpointsCount) { - if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED) count = count + 1 - } - } - - // we make sure all channels have set up their WatchConfirmed for the funding tx - awaitCond({ - val watches2 = nodes.values.foldLeft(Set.empty[Watch]) { - case (watches, setup) => - sender.send(setup.watcher, 'watches) - watches ++ sender.expectMsgType[Set[Watch]] - } - (watches2 -- watches1).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) @@ -371,6 +333,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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(scriptHash, _) :: OP_EQUAL :: Nil => + Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, scriptHash) case _ => ??? } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index dec759533..d8c02df51 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -66,11 +66,10 @@ object AnnouncementsBatchValidationSpec { val node2BitcoinKey = randomKey val amount = Satoshi(1000000) // first we publish the funding tx - val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient, null) + val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey))) - val fundingTxFuture = wallet.makeParentAndFundingTx(fundingPubkeyScript, amount, 10000) + val fundingTxFuture = wallet.makeFundingTx(fundingPubkeyScript, amount, 10000) val res = Await.result(fundingTxFuture, 10 seconds) - Await.result(extendedBitcoinClient.publishTransaction(res.parentTx), 10 seconds) Await.result(extendedBitcoinClient.publishTransaction(res.fundingTx), 10 seconds) SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, res.fundingTx, res.fundingTxOutputIndex) }