diff --git a/.travis.yml b/.travis.yml index c3a26d91..175dc976 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ sudo: required dist: trusty language: scala scala: - - 2.11.8 + - 2.11.11 env: - export LD_LIBRARY_PATH=/usr/local/lib script: diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 8f38ff18..bce1aa3f 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -5,7 +5,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-spv-SNAPSHOT eclair-core_2.11 @@ -140,6 +140,11 @@ jeromq 0.4.0 + + fr.acinq + bitcoinj-core + ${bitcoinj.version} + org.scodec @@ -187,12 +192,6 @@ ${akka.version} test - - com.google.guava - guava - 18.0 - test - ch.qos.logback logback-classic diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 4596f3ff..80b7841f 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -1,4 +1,8 @@ eclair { + + chain = "test" + spv = false // experimental!! do not use + server { public-ips = [] // external ips, will be announced on the network binding-ip = "0.0.0.0" @@ -16,13 +20,22 @@ eclair { zmq = "tcp://127.0.0.1:29000" } + bitcoinj { + static-peers = [ + #{ // currently used in integration tests to override default port + # host = "localhost" + # port = 28333 + #} + ] + } + node-alias = "eclair" node-color = "49daaa" global-features = "" local-features = "08" // initial_routing_sync channel-flags = 1 // announce channels dust-limit-satoshis = 542 - default-feerate-perkw = 10000 // corresponds to bitcoind's default value of feerate-perkB=20000 for a standard commit tx + default-feerate-per-kb = 20000 // default bitcoin core value max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited htlc-minimum-msat = 1000000 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 50b81a65..3f3b15da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -35,9 +35,10 @@ object Features { */ def areSupported(bitset: BitSet): Boolean = { // for now there is no mandatory feature bit, so we don't support features with any even bit set - bitset.stream().noneMatch(new IntPredicate { - override def test(value: Int) = value % 2 == 0 - }) + for(i <- 0 until bitset.length() by 2) { + if (bitset.get(i)) return false + } + return true } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Globals.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Globals.scala index eeb72f6c..c8cc1f39 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Globals.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Globals.scala @@ -11,14 +11,14 @@ object Globals { /** * This counter holds the current blockchain height. * It is mainly used to calculate htlc expiries. - * The value is updated by the [[fr.acinq.eclair.blockchain.PeerWatcher]] and read by all actors, hence it needs to be thread-safe. + * The value is updated by the [[fr.acinq.eclair.blockchain.ZmqWatcher]] and read by all actors, hence it needs to be thread-safe. */ val blockCount = new AtomicLong(0) /** * This counter holds the current feeratePerKw. * It is used to maintain an up-to-date fee in commitment tx so that they get confirmed fast enough. - * The value is updated by the [[fr.acinq.eclair.blockchain.PeerWatcher]] and read by all actors, hence it needs to be thread-safe. + * The value is updated by the [[fr.acinq.eclair.blockchain.ZmqWatcher]] and read by all actors, hence it needs to be thread-safe. */ val feeratePerKw = new AtomicLong(0) } 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 f050106a..60c1480d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -9,10 +9,9 @@ import java.util.concurrent.TimeUnit import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey -import fr.acinq.bitcoin.{BinaryData, DeterministicWallet} +import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqliteNetworkDb, SqlitePeersDb} -import org.sqlite.SQLiteConfig import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -39,7 +38,6 @@ case class NodeParams(extendedPrivateKey: ExtendedPrivateKey, feeProportionalMillionth: Int, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double, - defaultFinalScriptPubKey: BinaryData, channelsDb: ChannelsDb, peersDb: PeersDb, networkDb: NetworkDb, @@ -50,7 +48,8 @@ case class NodeParams(extendedPrivateKey: ExtendedPrivateKey, updateFeeMinDiffRatio: Double, autoReconnect: Boolean, chainHash: BinaryData, - channelFlags: Byte) + channelFlags: Byte, + spv: Boolean) object NodeParams { @@ -67,7 +66,7 @@ object NodeParams { .withFallback(overrideDefaults) .withFallback(ConfigFactory.load()).getConfig("eclair") - def makeNodeParams(datadir: File, config: Config, chainHash: BinaryData, defaultFinalScriptPubKey: BinaryData): NodeParams = { + def makeNodeParams(datadir: File, config: Config): NodeParams = { datadir.mkdirs() @@ -82,6 +81,13 @@ object NodeParams { val master = DeterministicWallet.generate(seed) val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil) + val chain = config.getString("chain") + val chainHash = chain match { + case "test" => Block.TestnetGenesisBlock.hash + case "regtest" => Block.RegtestGenesisBlock.hash + case _ => throw new RuntimeException("only regtest and testnet are supported for now") + } + val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(datadir, "eclair.sqlite")}") val channelsDb = new SqliteChannelsDb(sqlite) val peersDb = new SqlitePeersDb(sqlite) @@ -110,7 +116,6 @@ object NodeParams { feeProportionalMillionth = config.getInt("fee-proportional-millionth"), reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"), - defaultFinalScriptPubKey = defaultFinalScriptPubKey, channelsDb = channelsDb, peersDb = peersDb, networkDb = networkDb, @@ -121,6 +126,7 @@ object NodeParams { updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"), autoReconnect = config.getBoolean("auto-reconnect"), chainHash = chainHash, - channelFlags = config.getInt("channel-flags").toByte) + channelFlags = config.getInt("channel-flags").toByte, + spv = config.getBoolean("spv")) } } 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 de323345..c9b0a35d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -9,21 +9,23 @@ import akka.pattern.after import akka.stream.{ActorMaterializer, BindFailedException} import akka.util.Timeout import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.{Base58Check, BinaryData, Block, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Script} +import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.api.{GetInfoResponse, Service} -import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient +import fr.acinq.eclair.blockchain._ +import fr.acinq.eclair.blockchain.fee.{BitpayInsightFeeProvider, ConstantFeeProvider} +import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} +import fr.acinq.eclair.blockchain.spv.BitcoinjKit +import fr.acinq.eclair.blockchain.wallet.{BitcoinCoreWallet, BitcoinjWallet, EclairWallet} import fr.acinq.eclair.blockchain.zmq.ZMQActor -import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, PeerWatcher} import fr.acinq.eclair.channel.Register import fr.acinq.eclair.io.{Server, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ import grizzled.slf4j.Logging -import org.json4s.JsonAST.JString +import scala.collection.JavaConversions._ import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future, Promise} -import scala.util.Try /** * Created by PM on 25/01/2016. @@ -32,11 +34,19 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act logger.info(s"hello!") logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}") - val config = NodeParams.loadConfiguration(datadir, overrideDefaults) - // early check + val config = NodeParams.loadConfiguration(datadir, overrideDefaults) + val nodeParams = NodeParams.makeNodeParams(datadir, config) + val spv = config.getBoolean("spv") + val chain = config.getString("chain") + + // early checks + DBCompatChecker.checkDBCompatibility(nodeParams) PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port")) + logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}") + logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") + logger.info(s"initializing secure random generator") // this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala) secureRandom.nextInt() @@ -44,60 +54,70 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act implicit val system = actorSystem implicit val materializer = ActorMaterializer() implicit val timeout = Timeout(30 seconds) - - val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient( - user = config.getString("bitcoind.rpcuser"), - password = config.getString("bitcoind.rpcpassword"), - host = config.getString("bitcoind.host"), - port = config.getInt("bitcoind.rpcport"))) - implicit val formats = org.json4s.DefaultFormats implicit val ec = ExecutionContext.Implicits.global - val future = for { - json <- bitcoinClient.client.invoke("getblockchaininfo") - chain = (json \ "chain").extract[String] - blockCount = (json \ "blocks").extract[Long] - progress = (json \ "verificationprogress").extract[Double] - bitcoinVersion <- bitcoinClient.client.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String]) - } yield (chain, blockCount, progress, bitcoinVersion) - val (chain, blockCount, progress, bitcoinVersion) = Try(Await.result(future, 10 seconds)).recover { case _ => throw BitcoinRPCConnectionException }.get - val chainHash = chain match { - case "test" => Block.TestnetGenesisBlock.hash - case "regtest" => Block.RegtestGenesisBlock.hash - case _ => throw new RuntimeException("only regtest and testnet are supported for now") + val bitcoin = if (spv) { + logger.warn("EXPERIMENTAL SPV 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) + Left(bitcoinjKit) + } else { + val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient( + user = config.getString("bitcoind.rpcuser"), + password = config.getString("bitcoind.rpcpassword"), + host = config.getString("bitcoind.host"), + port = config.getInt("bitcoind.rpcport"))) + val future = for { + json <- bitcoinClient.rpcClient.invoke("getblockchaininfo").recover { case _ => throw BitcoinRPCConnectionException } + progress = (json \ "verificationprogress").extract[Double] + chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_)).map(x => BinaryData(x.reverse)) + bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String]) + } yield (progress, chainHash, bitcoinVersion) + // blocking sanity checks + val (progress, chainHash, bitcoinVersion) = Await.result(future, 10 seconds) + assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") + assert(progress > 0.99, "bitcoind should be synchronized") + // TODO: add a check on bitcoin version? + Right(bitcoinClient) } - logger.info(s"using chain=$chain chainHash=$chainHash") - assert(progress > 0.99, "bitcoind should be synchronized") - // we use it as final payment address, so that funds are moved to the bitcoind wallet upon channel termination - val JString(finalAddress) = Await.result(bitcoinClient.client.invoke("getnewaddress"), 10 seconds) - logger.info(s"finaladdress=$finalAddress") - // TODO: we should use p2wpkh instead of p2pkh as soon as bitcoind supports it - //val finalScriptPubKey = OP_0 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: Nil - val finalScriptPubKey = Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) - - val nodeParams = NodeParams.makeNodeParams(datadir, config, chainHash, finalScriptPubKey) - logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}") - - DBCompatChecker.checkDBCompatibility(nodeParams) - - Globals.blockCount.set(blockCount) - - val defaultFeeratePerKw = config.getLong("default-feerate-perkw") - val feeratePerKw = if (chain == "regtest") defaultFeeratePerKw else { - val feeratePerKB = Await.result(bitcoinClient.estimateSmartFee(nodeParams.smartfeeNBlocks), 10 seconds) - if (feeratePerKB < 0) defaultFeeratePerKw else feerateKB2Kw(feeratePerKB) - } - logger.info(s"initial feeratePerKw=$feeratePerKw") - Globals.feeratePerKw.set(feeratePerKw) - def bootstrap: Future[Kit] = { val zmqConnected = Promise[Boolean]() val tcpBound = Promise[Unit]() - val zmq = system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart)) - val watcher = system.actorOf(SimpleSupervisor.props(PeerWatcher.props(nodeParams, bitcoinClient), "watcher", SupervisorStrategy.Resume)) + val defaultFeeratePerKb = config.getLong("default-feerate-per-kb") + Globals.feeratePerKw.set(feerateKb2Kw(defaultFeeratePerKb)) + logger.info(s"initial feeratePerKw=${Globals.feeratePerKw.get()}") + val feeProvider = chain match { + case "regtest" => new ConstantFeeProvider(defaultFeeratePerKb) + case _ => new BitpayInsightFeeProvider() + } + system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeeratePerKB.map { + case feeratePerKB => + Globals.feeratePerKw.set(feerateKb2Kw(feeratePerKB)) + logger.info(s"current feeratePerKw=${Globals.feeratePerKw.get()}") + }) + + val watcher = bitcoin match { + case Left(bitcoinj) => + zmqConnected.success(true) + bitcoinj.startAsync() + system.actorOf(SimpleSupervisor.props(SpvWatcher.props(bitcoinj), "watcher", SupervisorStrategy.Resume)) + case Right(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)) + } + + val wallet = bitcoin match { + case Left(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet())) + case Right(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher) + } + wallet.getFinalAddress.map { + case address => logger.info(s"initial wallet address=$address") + } + val paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match { case "local" => LocalPaymentHandler.props(nodeParams) case "noop" => Props[NoopPaymentHandler] @@ -105,14 +125,13 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act val register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume)) val relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams.privateKey, paymentHandler), "relayer", SupervisorStrategy.Resume)) val router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume)) - val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer), "switchboard", SupervisorStrategy.Resume)) + val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume)) val paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.privateKey.publicKey, router, register), "payment-initiator", SupervisorStrategy.Restart)) val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, switchboard, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart)) val kit = Kit( nodeParams = nodeParams, system = system, - zmq = zmq, watcher = watcher, paymentHandler = paymentHandler, register = register, @@ -120,11 +139,12 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act router = router, switchboard = switchboard, paymentInitiator = paymentInitiator, - server = server) + server = server, + wallet = wallet) val api = new Service { - override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse(nodeId = nodeParams.privateKey.publicKey, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = chainHash, blockHeight = Globals.blockCount.intValue())) + override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse(nodeId = nodeParams.privateKey.publicKey, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue())) override def appKit = kit } @@ -148,7 +168,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act case class Kit(nodeParams: NodeParams, system: ActorSystem, - zmq: ActorRef, watcher: ActorRef, paymentHandler: ActorRef, register: ActorRef, @@ -156,7 +175,8 @@ case class Kit(nodeParams: NodeParams, router: ActorRef, switchboard: ActorRef, paymentInitiator: ActorRef, - server: ActorRef) + server: ActorRef, + wallet: EclairWallet) case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could not connect to bitcoind using zeromq") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 561965a1..b241b6fb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -103,10 +103,10 @@ trait Service extends Logging { for { req <- Future(PaymentRequest.read(paymentRequest)) amount = (req.amount, rest) match { - case (Some(amt), Nil) => amt.amount case (Some(_), JInt(amt) :: Nil) => amt.toLong // overriding payment request amount with the one provided + case (Some(amt), _) => amt.amount case (None, JInt(amt) :: Nil) => amt.toLong // amount wasn't specified in request, using custom one - case (None, Nil) => throw new RuntimeException("you need to manually specify an amount for this payment request") + case (None, _) => throw new RuntimeException("you need to manually specify an amount for this payment request") } res <- (paymentInitiator ? SendPayment(amount, req.paymentHash, req.nodeId)).mapTo[PaymentResult] } yield res diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala index 31254a1d..7d3a34b3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala @@ -15,5 +15,3 @@ case class NewTransaction(tx: Transaction) extends BlockchainEvent case class CurrentBlockCount(blockCount: Long) extends BlockchainEvent case class CurrentFeerate(feeratePerKw: Long) extends BlockchainEvent - -case class MempoolTransaction(tx: Transaction) \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/SpvWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/SpvWatcher.scala new file mode 100644 index 00000000..a70b275a --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/SpvWatcher.scala @@ -0,0 +1,184 @@ +package fr.acinq.eclair.blockchain + +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.{Satoshi, Transaction, TxIn, TxOut} +import fr.acinq.eclair.{Globals, fromShortId} +import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED +import fr.acinq.eclair.transactions.Scripts +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.concurrent.duration._ +import scala.util.{Failure, Success, Try} + +final case class Hint(script: Script) + +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 SpvWatcher(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]) + + context.system.scheduler.schedule(10 seconds, 1 minute, self, 'tick) + + 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=${Transaction.write(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)) + case w@WatchConfirmed(_, txId, minDepth, event) if txId == tx.txid && confirmations == -1 => + // the transaction watched was overriden by a competing tx + self ! TriggerEvent(w, WatchEventDoubleSpent(event)) + } + 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 hint: Hint => { + Context.propagate(kit.wallet.getContext) + kit.wallet().addWatchedScripts(ImmutableList.of(hint.script)) + } + + case w: Watch if !watches.contains(w) => + log.debug(s"adding watch $w for $sender") + log.warning(s"resending ${oldEvents.size} events in order!") + 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=${Transaction.write(tx)}") + self ! WatchConfirmed(self, parentTxid, 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, tx +: block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction])) + 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, tx +: block2tx.getOrElse(absTimeout, Seq.empty[Transaction])) + context.become(watching(watches, block2tx1, oldEvents, sent)) + } else publish(tx) + + case ParallelGetRequest(announcements) => sender ! ParallelGetResponse(announcements.map { + case 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) + IndividualResult(c, Some(fakeFundingTx), true) + }) + + 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 + + } + + def publish(tx: Transaction): Unit = broadcaster ! tx + +} + +object SpvWatcher { + + def props(kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new SpvWatcher(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/blockchain/Types.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/Types.scala index 1d3a2f1d..5b17111e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/Types.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/Types.scala @@ -29,13 +29,12 @@ final case class WatchEventConfirmed(event: BitcoinEvent, blockHeight: Int, txIn final case class WatchEventSpent(event: BitcoinEvent, tx: Transaction) extends WatchEvent final case class WatchEventSpentBasic(event: BitcoinEvent) extends WatchEvent final case class WatchEventLost(event: BitcoinEvent) extends WatchEvent +final case class WatchEventDoubleSpent(event: BitcoinEvent) extends WatchEvent /** * Publish the provided tx as soon as possible depending on locktime and csv */ final case class PublishAsap(tx: Transaction) -final case class MakeFundingTx(localCommitPub: PublicKey, remoteCommitPub: PublicKey, amount: Satoshi, feeRatePerKw: Long) -final case class MakeFundingTxResponse(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey) final case class ParallelGetRequest(ann: Seq[ChannelAnnouncement]) final case class IndividualResult(c: ChannelAnnouncement, tx: Option[Transaction], unspent: Boolean) final case class ParallelGetResponse(r: Seq[IndividualResult]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/PeerWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/ZmqWatcher.scala similarity index 93% rename from eclair-core/src/main/scala/fr/acinq/eclair/blockchain/PeerWatcher.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/blockchain/ZmqWatcher.scala index 0f525896..07179246 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/PeerWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/ZmqWatcher.scala @@ -5,9 +5,10 @@ import java.util.concurrent.Executors import akka.actor.{Actor, ActorLogging, Cancellable, Props, Terminated} import akka.pattern.pipe import fr.acinq.bitcoin._ +import fr.acinq.eclair.blockchain.rpc.ExtendedBitcoinClient import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.{Globals, NodeParams, feerateKB2Kw} +import fr.acinq.eclair.{Globals, NodeParams} import scala.collection.SortedMap import scala.concurrent.duration._ @@ -20,16 +21,21 @@ import scala.util.Try * - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs) * Created by PM on 21/02/2016. */ -class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { +class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { context.system.eventStream.subscribe(self, classOf[BlockchainEvent]) + // this is to initialize block count + self ! 'tick + case class TriggerEvent(w: Watch, e: WatchEvent) def receive: Receive = watching(Set(), SortedMap(), None) def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], nextTick: Option[Cancellable]): Receive = { + case hint: Hint => {} + case NewTransaction(tx) => //log.debug(s"analyzing txid=${tx.txid} tx=${Transaction.write(tx)}") watches.collect { @@ -55,14 +61,14 @@ class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implici Globals.blockCount.set(count) context.system.eventStream.publish(CurrentBlockCount(count)) } - client.estimateSmartFee(nodeParams.smartfeeNBlocks).map { + /*client.estimateSmartFee(nodeParams.smartfeeNBlocks).map { case feeratePerKB if feeratePerKB > 0 => val feeratePerKw = feerateKB2Kw(feeratePerKB) log.debug(s"setting feeratePerKB=$feeratePerKB -> feeratePerKw=$feeratePerKw") Globals.feeratePerKw.set(feeratePerKw) context.system.eventStream.publish(CurrentFeerate(feeratePerKw)) case _ => () // bitcoind cannot estimate feerate - } + }*/ // TODO: beware of the herd effect watches.collect { case w@WatchConfirmed(_, txId, minDepth, event) => @@ -117,9 +123,6 @@ class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implici context.become(watching(watches, block2tx1, None)) } else publish(tx) - case MakeFundingTx(ourCommitPub, theirCommitPub, amount, feeRatePerKw) => - client.makeFundingTx(ourCommitPub, theirCommitPub, amount, feeRatePerKw).pipeTo(sender) - case ParallelGetRequest(ann) => client.getParallel(ann).pipeTo(sender) case Terminated(channel) => @@ -195,8 +198,8 @@ class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implici } -object PeerWatcher { +object ZmqWatcher { - def props(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new PeerWatcher(nodeParams, client)(ec)) + def props(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new ZmqWatcher(client)(ec)) } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala new file mode 100644 index 00000000..1f9d0764 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala @@ -0,0 +1,33 @@ +package fr.acinq.eclair.blockchain.fee +import fr.acinq.bitcoin.Btc +import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient +import org.json4s.JsonAST.{JDouble, JInt} + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Created by PM on 09/07/2017. + */ +class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeeratePerKB: Long)(implicit ec: ExecutionContext) extends FeeProvider { + + /** + * We need this to keep commitment tx fees in sync with the state of the network + * + * @param nBlocks number of blocks until tx is confirmed + * @return the current + */ + def estimateSmartFee(nBlocks: Int): Future[Long] = + rpcClient.invoke("estimatesmartfee", nBlocks).map(json => { + json \ "feerate" match { + case JDouble(feerate) => Btc(feerate).toLong + case JInt(feerate) if feerate.toLong < 0 => feerate.toLong + case JInt(feerate) => Btc(feerate.toLong).toLong + } + }) + + override def getFeeratePerKB: Future[Long] = estimateSmartFee(3).map { + case f if f < 0 => defaultFeeratePerKB + case f => f + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitpayInsightFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitpayInsightFeeProvider.scala new file mode 100644 index 00000000..ec79ce01 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitpayInsightFeeProvider.scala @@ -0,0 +1,33 @@ +package fr.acinq.eclair.blockchain.fee + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.ActorMaterializer +import fr.acinq.bitcoin.{Btc, Satoshi} +import org.json4s.JsonAST.{JDouble, JValue} +import org.json4s.{DefaultFormats, jackson} +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.ActorMaterializer +import de.heikoseeberger.akkahttpjson4s.Json4sSupport._ + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Created by PM on 09/07/2017. + */ +class BitpayInsightFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider { + + implicit val materializer = ActorMaterializer() + val httpClient = Http(system) + implicit val serialization = jackson.Serialization + implicit val formats = DefaultFormats + + override def getFeeratePerKB: Future[Long] = + for { + httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://test-insight.bitpay.com/api/utils/estimatefee?nbBlocks=3"), method = HttpMethods.GET)) + json <- Unmarshal(httpRes).to[JValue] + JDouble(fee_per_kb) = json \ "3" + } yield (Btc(fee_per_kb): Satoshi).amount +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/ConstantFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/ConstantFeeProvider.scala new file mode 100644 index 00000000..cf313d48 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/ConstantFeeProvider.scala @@ -0,0 +1,10 @@ +package fr.acinq.eclair.blockchain.fee +import scala.concurrent.Future + +/** + * Created by PM on 09/07/2017. + */ +class ConstantFeeProvider(feeratePerKB: Long) extends FeeProvider { + + override def getFeeratePerKB: Future[Long] = Future.successful(feeratePerKB) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala new file mode 100644 index 00000000..d0d1de26 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -0,0 +1,12 @@ +package fr.acinq.eclair.blockchain.fee + +import scala.concurrent.Future + +/** + * Created by PM on 09/07/2017. + */ +trait FeeProvider { + + def getFeeratePerKB: Future[Long] + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/rpc/ExtendedBitcoinClient.scala similarity index 52% rename from eclair-core/src/main/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClient.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/blockchain/rpc/ExtendedBitcoinClient.scala index cbf0bda9..d4264275 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/rpc/ExtendedBitcoinClient.scala @@ -1,11 +1,8 @@ -package fr.acinq.eclair.blockchain +package fr.acinq.eclair.blockchain.rpc -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin._ -import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, JsonRPCError} -import fr.acinq.eclair.channel.Helpers +import fr.acinq.eclair.blockchain.{IndividualResult, ParallelGetResponse} import fr.acinq.eclair.fromShortId -import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.ChannelAnnouncement import org.json4s.JsonAST._ @@ -15,9 +12,7 @@ import scala.util.Try /** * Created by PM on 26/04/2016. */ -class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { - - import ExtendedBitcoinClient._ +class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) { implicit val formats = org.json4s.DefaultFormats @@ -29,14 +24,14 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { def hex2tx(hex: String): Transaction = Transaction.read(hex, protocolVersion) def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] = - client.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations + rpcClient.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations .map(json => Some((json \ "confirmations").extractOrElse[Int](0))) .recover { case t: JsonRPCError if t.error.code == -5 => None } def getTxBlockHash(txId: String)(implicit ec: ExecutionContext): Future[Option[String]] = - client.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations + rpcClient.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations .map(json => (json \ "blockhash").extractOpt[String]) .recover { case t: JsonRPCError if t.error.code == -5 => None @@ -44,7 +39,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { def getBlockHashesSinceBlockHash(blockHash: String, previous: Seq[String] = Nil)(implicit ec: ExecutionContext): Future[Seq[String]] = for { - nextblockhash_opt <- client.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String])) + nextblockhash_opt <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String])) res <- nextblockhash_opt match { case Some(nextBlockHash) => getBlockHashesSinceBlockHash(nextBlockHash, previous :+ nextBlockHash) case None => Future.successful(previous) @@ -53,7 +48,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { def getTxsSinceBlockHash(blockHash: String, previous: Seq[Transaction] = Nil)(implicit ec: ExecutionContext): Future[Seq[Transaction]] = for { - (nextblockhash_opt, txids) <- client.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String], (json \ "tx").extract[List[String]])) + (nextblockhash_opt, txids) <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String], (json \ "tx").extract[List[String]])) next <- Future.sequence(txids.map(getTransaction(_))) res <- nextblockhash_opt match { case Some(nextBlockHash) => getTxsSinceBlockHash(nextBlockHash, previous ++ next) @@ -63,7 +58,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { def getMempool()(implicit ec: ExecutionContext): Future[Seq[Transaction]] = for { - txids <- client.invoke("getrawmempool").map(json => json.extract[List[String]]) + txids <- rpcClient.invoke("getrawmempool").map(json => json.extract[List[String]]) txs <- Future.sequence(txids.map(getTransaction(_))) } yield txs @@ -78,7 +73,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { * @return a Future[txid] where txid (a String) is the is of the tx that sends the bitcoins */ def sendFromAccount(account: String, destination: String, amount: Double)(implicit ec: ExecutionContext): Future[String] = - client.invoke("sendfrom", account, destination, amount) collect { + rpcClient.invoke("sendfrom", account, destination, amount) collect { case JString(txid) => txid } @@ -88,7 +83,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { * @return */ def getRawTransaction(txId: String)(implicit ec: ExecutionContext): Future[String] = - client.invoke("getrawtransaction", txId) collect { + rpcClient.invoke("getrawtransaction", txId) collect { case JString(raw) => raw } @@ -97,8 +92,8 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { def getTransaction(height: Int, index: Int)(implicit ec: ExecutionContext): Future[Transaction] = for { - hash <- client.invoke("getblockhash", height).map(json => json.extract[String]) - json <- client.invoke("getblock", hash) + hash <- rpcClient.invoke("getblockhash", height).map(json => json.extract[String]) + json <- rpcClient.invoke("getblock", hash) JArray(txs) = json \ "tx" txid = txs(index).extract[String] tx <- getTransaction(txid) @@ -106,7 +101,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { def isTransactionOuputSpendable(txId: String, ouputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = for { - json <- client.invoke("gettxout", txId, ouputIndex, includeMempool) + json <- rpcClient.invoke("gettxout", txId, ouputIndex, includeMempool) } yield json != JNull @@ -120,7 +115,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { def getTransactionShortId(txId: String)(implicit ec: ExecutionContext): Future[(Int, Int)] = { val future = for { Some(blockHash) <- getTxBlockHash(txId) - json <- client.invoke("getblock", blockHash) + json <- rpcClient.invoke("getblock", blockHash) JInt(height) = json \ "height" JString(hash) = json \ "hash" JArray(txs) = json \ "tx" @@ -130,60 +125,14 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { future } - def fundTransaction(hex: String)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { - client.invoke("fundrawtransaction", hex /*hex.take(4) + "0000" + hex.drop(4)*/).map(json => { - val JString(hex) = json \ "hex" - val JInt(changepos) = json \ "changepos" - val JDouble(fee) = json \ "fee" - FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee) - }) - } - - def fundTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = - fundTransaction(tx2Hex(tx)) - - def signTransaction(hex: String)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = - client.invoke("signrawtransaction", hex).map(json => { - val JString(hex) = json \ "hex" - val JBool(complete) = json \ "complete" - SignTransactionResponse(Transaction.read(hex), complete) - }) - - def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = - signTransaction(tx2Hex(tx)) - def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = - client.invoke("sendrawtransaction", hex) collect { + rpcClient.invoke("sendrawtransaction", hex) collect { case JString(txid) => txid } def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = publishTransaction(tx2Hex(tx)) - - def makeFundingTx(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, amount: Satoshi, feeRatePerKw: Long)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = - for { - // ask for a new address and the corresponding private key - JString(address) <- client.invoke("getnewaddress") - JString(wif) <- client.invoke("dumpprivkey", address) - JString(segwitAddress) <- client.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) - // 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 - (partialFundingTx, _) = Transactions.makePartialFundingTx(amount, localFundingPubkey, remoteFundingPubkey) - // 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 Helpers.Funding.sign(MakeFundingTxResponse(parentTx, unsignedFundingTx, 0, priv)) - - /** * We need this to compute absolute timeouts expressed in number of blocks (where getBlockCount would be equivalent * to time.now()) @@ -192,26 +141,10 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { * @return the current number of blocks in the active chain */ def getBlockCount(implicit ec: ExecutionContext): Future[Long] = - client.invoke("getblockcount") collect { + rpcClient.invoke("getblockcount") collect { case JInt(count) => count.toLong } - /** - * We need this to keep commitment tx fees in sync with the state of the network - * - * @param nBlocks number of blocks until tx is confirmed - * @param ec - * @return the current - */ - def estimateSmartFee(nBlocks: Int)(implicit ec: ExecutionContext): Future[Long] = - client.invoke("estimatesmartfee", nBlocks).map(json => { - json \ "feerate" match { - case JDouble(feerate) => Btc(feerate).toLong - case JInt(feerate) if feerate.toLong < 0 => feerate.toLong - case JInt(feerate) => Btc(feerate.toLong).toLong - } - }) - def getParallel(awaiting: Seq[ChannelAnnouncement]): Future[ParallelGetResponse] = { case class TxCoordinate(blockHeight: Int, txIndex: Int, outputIndex: Int) @@ -225,8 +158,8 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { implicit val formats = org.json4s.DefaultFormats for { - blockHashes: Seq[String] <- client.invoke(coordinates.map(coord => ("getblockhash", coord._1.blockHeight :: Nil))).map(_.map(_.extractOrElse[String]("00" * 32))) - txids: Seq[String] <- client.invoke(blockHashes.map(h => ("getblock", h :: Nil))) + blockHashes: Seq[String] <- rpcClient.invoke(coordinates.map(coord => ("getblockhash", coord._1.blockHeight :: Nil))).map(_.map(_.extractOrElse[String]("00" * 32))) + txids: Seq[String] <- rpcClient.invoke(blockHashes.map(h => ("getblock", h :: Nil))) .map(_.zipWithIndex) .map(_.map { case (json, idx) => Try { @@ -234,24 +167,16 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) { txs(coordinates(idx)._1.txIndex).extract[String] } getOrElse ("00" * 32) }) - txs <- client.invoke(txids.map(txid => ("getrawtransaction", txid :: Nil))).map(_.map { + txs <- rpcClient.invoke(txids.map(txid => ("getrawtransaction", txid :: Nil))).map(_.map { case JString(raw) => Some(Transaction.read(raw)) case _ => None }) - unspent <- client.invoke(txids.zipWithIndex.map(txid => ("gettxout", txid._1 :: coordinates(txid._2)._1.outputIndex :: true :: Nil))).map(_.map(_ != JNull)) + unspent <- rpcClient.invoke(txids.zipWithIndex.map(txid => ("gettxout", txid._1 :: coordinates(txid._2)._1.outputIndex :: true :: Nil))).map(_.map(_ != JNull)) } yield ParallelGetResponse(awaiting.zip(txs.zip(unspent)).map(x => IndividualResult(x._1, x._2._1, x._2._2))) } } -object ExtendedBitcoinClient { - - case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double) - - case class SignTransactionResponse(tx: Transaction, complete: Boolean) - -} - /*object Test extends App { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/spv/BitcoinjKit.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/spv/BitcoinjKit.scala new file mode 100644 index 00000000..ed94c514 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/spv/BitcoinjKit.scala @@ -0,0 +1,140 @@ +package fr.acinq.eclair.blockchain.spv + +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.spv.BitcoinjKit._ +import fr.acinq.eclair.blockchain.{CurrentBlockCount, NewConfidenceLevel} +import grizzled.slf4j.Logging +import org.bitcoinj.core.TransactionConfidence.ConfidenceType +import org.bitcoinj.core.listeners._ +import org.bitcoinj.core.{Block, FilteredBlock, NetworkParameters, Peer, PeerAddress, StoredBlock, 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}") + +// 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 = + // 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 = { + logger.info(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 { + fullBlock.getTransactions.foreach { + case tx => + logger.info(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 = { + 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/wallet/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/BitcoinCoreWallet.scala new file mode 100644 index 00000000..31c4d41e --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/BitcoinCoreWallet.scala @@ -0,0 +1,177 @@ +package fr.acinq.eclair.blockchain.wallet + +import akka.actor.{Actor, ActorRef, ActorSystem, Props} +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{Base58Check, BinaryData, Crypto, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import fr.acinq.eclair.blockchain._ +import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient +import fr.acinq.eclair.channel.{BITCOIN_INPUT_SPENT, BITCOIN_TX_CONFIRMED, Helpers} +import fr.acinq.eclair.transactions.Transactions +import grizzled.slf4j.Logging +import org.json4s.JsonAST.{JBool, JDouble, JInt, JString} + +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future, Promise} + +/** + * Created by PM on 06/07/2017. + */ +class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging { + + override def getBalance: Future[Satoshi] = ??? + + 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): Future[FundTransactionResponse] = { + rpcClient.invoke("fundrawtransaction", hex).map(json => { + val JString(hex) = json \ "hex" + val JInt(changepos) = json \ "changepos" + val JDouble(fee) = json \ "fee" + FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee) + }) + } + + def fundTransaction(tx: Transaction): Future[FundTransactionResponse] = + fundTransaction(Transaction.write(tx).toString()) + + def signTransaction(hex: String): Future[SignTransactionResponse] = + rpcClient.invoke("signrawtransaction", hex).map(json => { + val JString(hex) = json \ "hex" + val JBool(complete) = json \ "complete" + SignTransactionResponse(Transaction.read(hex), complete) + }) + + def signTransaction(tx: Transaction): Future[SignTransactionResponse] = + signTransaction(Transaction.write(tx).toString()) + + /** + * + * @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) + + 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) + } + + /** + * + * @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) + + // 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] = + 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) + // 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 + 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) + _ = logger.debug(s"built parentTxid=${parentTx.txid}, initializing temporary actor") + tempActor = system.actorOf(Props(new Actor { + override def receive: Receive = { + case WatchEventSpent(BITCOIN_INPUT_SPENT(parentTx), 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, 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 + input0 = parentTx.txIn.head + _ = watcher ! WatchSpent(tempActor, input0.outPoint.txid, input0.outPoint.index.toInt, BITCOIN_INPUT_SPENT(parentTx)) + // 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 don't manage double spends yet + * @param tx + * @return + */ + override def commit(tx: Transaction): Future[Boolean] = Future.successful(true) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/BitcoinjWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/BitcoinjWallet.scala new file mode 100644 index 00000000..93f7b01e --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/BitcoinjWallet.scala @@ -0,0 +1,59 @@ +package fr.acinq.eclair.blockchain.wallet + +import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction} +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 + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/EclairWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/EclairWallet.scala new file mode 100644 index 00000000..28236f23 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/wallet/EclairWallet.scala @@ -0,0 +1,22 @@ +package fr.acinq.eclair.blockchain.wallet + +import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction} + +import scala.concurrent.Future + +/** + * Created by PM on 06/07/2017. + */ +trait EclairWallet { + + def getBalance: Future[Satoshi] + + def getFinalAddress: Future[String] + + def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] + + def commit(tx: Transaction): Future[Boolean] + +} + +final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int) 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 2c649a61..3c6cdd10 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 @@ -1,21 +1,24 @@ package fr.acinq.eclair.channel -import akka.actor.{ActorRef, DiagnosticActorLogging, FSM, LoggingFSM, OneForOneStrategy, Props, Status, SupervisorStrategy} +import akka.actor.{ActorRef, FSM, LoggingFSM, OneForOneStrategy, Props, Status, SupervisorStrategy} import akka.event.Logging.MDC -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import akka.pattern.pipe +import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ +import fr.acinq.eclair.blockchain.wallet.{EclairWallet, MakeFundingTxResponse} import fr.acinq.eclair.channel.Helpers.{Closing, Funding} import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx} 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, Success, Try} +import scala.util.{Failure, Left, Random, Success, Try} /** @@ -23,10 +26,10 @@ import scala.util.{Failure, Left, Success, Try} */ object Channel { - def props(nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef) = Props(new Channel(nodeParams, remoteNodeId, blockchain, router, relayer)) + def props(nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef) = Props(new Channel(nodeParams, wallet, remoteNodeId, blockchain, router, relayer)) } -class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends LoggingFSM[State, Data] with FSMDiagnosticActorLogging[State, Data] { +class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends LoggingFSM[State, Data] with FSMDiagnosticActorLogging[State, Data] { val forwarder = context.actorOf(Props(new Forwarder(nodeParams)), "forwarder") @@ -102,6 +105,9 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A log.info(s"restoring channel $data") context.system.eventStream.publish(ChannelRestored(self, context.parent, remoteNodeId, data.commitments.localParams.isFunder, data.channelId, data)) // TODO: should we wait for an acknowledgment from the watcher? + if (nodeParams.spv) { + blockchain ! Hint(new BitcoinjScript(data.commitments.commitInput.txOut.publicKeyScript)) + } blockchain ! WatchSpent(self, data.commitments.commitInput.outPoint.txid, data.commitments.commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) blockchain ! WatchLost(self, data.commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) data match { @@ -202,9 +208,8 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A localFeatures = remoteInit.localFeatures) log.debug(s"remote params: $remoteParams") val localFundingPubkey = localParams.fundingPrivKey.publicKey - // we assume that our funding parent tx is about 250 bytes, that the feereate-per-kb is 2*feerate-per-kw and we double the fee estimate - // to give the parent a hefty fee - blockchain ! MakeFundingTx(localFundingPubkey, remoteParams.fundingPubKey, Satoshi(fundingSatoshis), Globals.feeratePerKw.get()) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey))) + wallet.makeFundingTx(fundingPubkeyScript, Satoshi(fundingSatoshis), Globals.feeratePerKw.get()).pipeTo(self) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open) } @@ -216,57 +221,27 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A }) when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(fundingResponse@MakeFundingTxResponse(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey), data@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, _)) => - // we watch the first input of the parent tx, so that we can detect when it is spent by a malleated avatar - val input0 = parentTx.txIn.head - blockchain ! WatchSpent(self, input0.outPoint.txid, input0.outPoint.index.toInt, BITCOIN_INPUT_SPENT(parentTx)) - // and we publish the parent tx - log.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) - context.system.scheduler.scheduleOnce(100 milliseconds, blockchain, PublishAsap(parentTx)) - goto(WAIT_FOR_FUNDING_PARENT) using DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, Set(parentTx), data) + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex), data@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) => + // let's create the first commitment tx that spends the yet uncommitted funding tx + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) + require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") + val localSigOfRemoteTx = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey) + // signature of their initial commitment tx that pays remote pushMsat + val fundingCreated = FundingCreated( + temporaryChannelId = temporaryChannelId, + fundingTxid = fundingTx.hash, + fundingOutputIndex = fundingTxOutputIndex, + signature = localSigOfRemoteTx + ) + val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) + context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId)) + goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated - case Event(CMD_CLOSE(_), _) => goto(CLOSED) - - case Event(e: Error, _) => handleRemoteErrorNoCommitments(e) - - case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_PARENT)(handleExceptions { - case Event(WatchEventSpent(BITCOIN_INPUT_SPENT(parentTx), spendingTx), DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, parentCandidates, data)) => - 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 - log.warning(s"parent tx has been malleated: originalParentTxid=${parentTx.txid} malleated=${spendingTx.txid}") - } - blockchain ! WatchConfirmed(self, spendingTx.txid, minDepth = 1, BITCOIN_TX_CONFIRMED(spendingTx)) - stay using DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, parentCandidates + spendingTx, data) - - case Event(WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _), DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, _, data)) => - // a potential parent for our funding tx has been confirmed, let's update our funding tx - Try(Helpers.Funding.replaceParent(fundingResponse, tx)) match { - case Success(MakeFundingTxResponse(_, fundingTx, fundingTxOutputIndex, _)) => - // let's create the first commitment tx that spends the yet uncommitted funding tx - import data._ - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) - - val localSigOfRemoteTx = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey) - // signature of their initial commitment tx that pays remote pushMsat - val fundingCreated = FundingCreated( - temporaryChannelId = temporaryChannelId, - fundingTxid = fundingTx.hash, - fundingOutputIndex = fundingTxOutputIndex, - signature = localSigOfRemoteTx - ) - val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) - context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId)) - goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), data.lastSent.channelFlags, fundingCreated) sending fundingCreated - case Failure(cause) => - log.warning(s"confirmed tx ${tx.txid} is not an input to our funding tx") - stay() - } + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + log.error(t, s"wallet returned error: ") + val error = Error(d.temporaryChannelId, "aborting channel creation".getBytes) + goto(CLOSED) sending error case Event(CMD_CLOSE(_), _) => goto(CLOSED) @@ -294,8 +269,6 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) // watch the funding tx transaction val commitInput = localCommitTx.input - blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? - blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) val fundingSigned = FundingSigned( channelId = channelId, signature = localSigOfRemoteTx @@ -307,6 +280,11 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, commitInput, ShaChain.init, channelId = channelId) log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") + if (nodeParams.spv) { + blockchain ! Hint(new BitcoinjScript(commitments.commitInput.txOut.publicKeyScript)) + } + blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? + blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId)) context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) @@ -325,7 +303,8 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A // we make sure that their sig checks out and that our first commit tx is spendable val localSigOfLocalTx = Transactions.sign(localCommitTx, localParams.fundingPrivKey) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { + Transactions.checkSpendable(fundingTx, signedLocalCommitTx.tx) + .flatMap(_ => Transactions.checkSpendable(signedLocalCommitTx)) match { case Failure(cause) => log.error(cause, "their FundingSigned message contains an invalid signature") val error = Error(channelId, cause.getMessage.getBytes) @@ -343,9 +322,18 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") // we do this to make sure that the channel state has been written to disk when we publish the funding tx val nextState = store(DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, Left(fundingCreated))) - blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? - blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) - blockchain ! PublishAsap(fundingTx) + if (nodeParams.spv) { + blockchain ! Hint(new BitcoinjScript(commitments.commitInput.txOut.publicKeyScript)) + } + log.info(s"committing txid=${fundingTx.txid}") + wallet.commit(fundingTx).map { + case true => + blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? + blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) + blockchain ! PublishAsap(fundingTx) + case false => + self ! WatchEventDoubleSpent(BITCOIN_FUNDING_DEPTHOK) + } goto(WAIT_FOR_FUNDING_CONFIRMED) using nextState } @@ -386,9 +374,15 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A // this clock will be used to detect htlc timeouts context.system.eventStream.subscribe(self, classOf[CurrentBlockCount]) context.system.eventStream.subscribe(self, classOf[CurrentFeerate]) - if (d.commitments.announceChannel) { + // NB: in spv 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 + if (d.commitments.announceChannel && !nodeParams.spv) { // used for announcement of channel (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) + } else if (d.commitments.announceChannel && nodeParams.spv && d.commitments.localParams.isFunder && System.getProperty("spvtest") != null) { + // hard coded id for testing + log.warning("using hardcoded short id for testing!!!!!") + context.system.scheduler.scheduleOnce(5 seconds, self, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, Random.nextInt(100), Random.nextInt(100))) } goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), None, None, None, None)) @@ -650,6 +644,11 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A log.info(s"received remote announcement signatures, delaying") // our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) + if (nodeParams.spv) { + log.warning(s"HACK: since we cannot get the tx index in spv mode, we copy the value sent by remote") + val (blockHeight, txIndex, _) = fromShortId(remoteAnnSigs.shortChannelId) + self ! WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex) + } stay } @@ -995,7 +994,7 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A log.info(s"re-sending fundingLocked") val nextPerCommitmentPoint = Generators.perCommitPoint(d.commitments.localParams.shaSeed, 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) - goto(WAIT_FOR_FUNDING_LOCKED) sending (fundingLocked) + goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => @@ -1017,7 +1016,9 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE - if (d.commitments.announceChannel && d.shortChannelId.isEmpty) { + // NB: in spv 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 + if (d.commitments.announceChannel && d.shortChannelId.isEmpty && !nodeParams.spv) { blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) } @@ -1089,7 +1090,8 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A // we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream case Event(CurrentFeerate(_), _) => stay - case Event("ok", _) => stay // noop handler + // we receive this when we send command to ourselves + case Event("ok", _) => stay } onTransition { @@ -1174,10 +1176,21 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A // NB: we do not watch for htlcs txes!! // this may lead to some htlcs not been claimed because the channel will be considered close and deleted before the claiming txes are published localCommitPublished.claimMainDelayedOutputTx match { - case Some(tx) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_LOCALCOMMIT_DONE) + case Some(tx) => + if (nodeParams.spv) { + // we need to watch the corresponding public key script of the commit tx + blockchain ! Hint(new BitcoinjScript(localCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript)) + } + blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_LOCALCOMMIT_DONE) case None => blockchain ! WatchConfirmed(self, localCommitPublished.commitTx.txid, nodeParams.minDepthBlocks, BITCOIN_LOCALCOMMIT_DONE) } + // we need to watch the htlc-success outputs in order to be notified when they can be spent by claim-delayed-output txes + if (nodeParams.spv) { + // we need to watch the corresponding public key script of the htlc-success tx + localCommitPublished.htlcSuccessTxs.map(_.txIn(0).outPoint.index.toInt).map(outputIndex => blockchain ! Hint(new BitcoinjScript(localCommitPublished.commitTx.txOut(outputIndex).publicKeyScript))) + } + localCommitPublished.claimMainDelayedOutputTx.foreach(tx => blockchain ! PublishAsap(tx)) localCommitPublished.htlcSuccessTxs.foreach(tx => blockchain ! PublishAsap(tx)) localCommitPublished.htlcTimeoutTxs.foreach(tx => blockchain ! PublishAsap(tx)) @@ -1187,6 +1200,10 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A localCommitPublished.htlcTimeoutTxs.foreach(tx => { require(tx.txIn.size == 1, s"an htlc-timeout tx must have exactly 1 input (has ${tx.txIn.size})") val outpoint = tx.txIn(0).outPoint + if (nodeParams.spv) { + // we need to watch the corresponding public key script of the commit tx + blockchain ! Hint(new BitcoinjScript(localCommitPublished.commitTx.txOut(outpoint.index.toInt).publicKeyScript)) + } log.info(s"watching output ${outpoint.index} of commit tx ${outpoint.txid}") blockchain ! WatchSpent(relayer, outpoint.txid, outpoint.index.toInt, BITCOIN_HTLC_SPENT) }) @@ -1231,7 +1248,12 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A // NB: we do not watch for htlcs txes!! // this may lead to some htlcs not been claimed because the channel will be considered close and deleted before the claiming txes are published remoteCommitPublished.claimMainOutputTx match { - case Some(tx) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, event) + case Some(tx) => + if (nodeParams.spv) { + // we need to watch the corresponding public key script of the commit tx + blockchain ! Hint(new BitcoinjScript(remoteCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript)) + } + blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, event) case None => blockchain ! WatchConfirmed(self, remoteCommitPublished.commitTx.txid, nodeParams.minDepthBlocks, event) } @@ -1243,6 +1265,10 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A remoteCommitPublished.claimHtlcTimeoutTxs.foreach(tx => { require(tx.txIn.size == 1, s"a claim-htlc-timeout tx must have exactly 1 input (has ${tx.txIn.size})") val outpoint = tx.txIn(0).outPoint + if (nodeParams.spv) { + // we need to watch the corresponding public key script of the commit tx + blockchain ! Hint(new BitcoinjScript(remoteCommitPublished.commitTx.txOut(outpoint.index.toInt).publicKeyScript)) + } log.info(s"watching output ${outpoint.index} of commit tx ${outpoint.txid}") blockchain ! WatchSpent(relayer, outpoint.txid, outpoint.index.toInt, BITCOIN_HTLC_SPENT) }) @@ -1275,8 +1301,18 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A // if there is a main-penalty or a claim-main-output tx, we watch them, otherwise we watch the commit tx // NB: we do not watch for htlcs txes, but we don't steal them currently anyway (revokedCommitPublished.mainPenaltyTx, revokedCommitPublished.claimMainOutputTx) match { - case (Some(tx), _) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE) - case (None, Some(tx)) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE) + case (Some(tx), _) => + if (nodeParams.spv) { + // we need to watch the corresponding public key script of the revoked commit tx + blockchain ! Hint(new BitcoinjScript(revokedCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript)) + } + blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE) + case (None, Some(tx)) => + if (nodeParams.spv) { + // we need to watch the corresponding public key script of the revoked commit tx + blockchain ! Hint(new BitcoinjScript(revokedCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript)) + } + blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE) case _ => blockchain ! WatchConfirmed(self, revokedCommitPublished.commitTx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE) } @@ -1416,15 +1452,6 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A state } - // def storing(): FSM.State[fr.acinq.eclair.channel.State, Data] = { - // state.stateData match { - // case d: HasCommitments => - // log.debug(s"updating database record for channelId=${d.channelId} (state=$state)") - // nodeParams.channelsDb.put(d.channelId, d) - // case _ => {} - // } - // state - // } } override def mdc(currentMessage: Any): MDC = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 9d6f61ec..d2c74dca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -4,7 +4,6 @@ import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{BinaryData, Transaction} import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.blockchain.MakeFundingTxResponse import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx @@ -32,7 +31,6 @@ case object WAIT_FOR_INIT_INTERNAL extends State case object WAIT_FOR_OPEN_CHANNEL extends State case object WAIT_FOR_ACCEPT_CHANNEL extends State case object WAIT_FOR_FUNDING_INTERNAL extends State -case object WAIT_FOR_FUNDING_PARENT extends State case object WAIT_FOR_FUNDING_CREATED extends State case object WAIT_FOR_FUNDING_SIGNED extends State case object WAIT_FOR_FUNDING_CONFIRMED extends State @@ -135,7 +133,6 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, lastSent: OpenChannel) extends Data -final case class DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse: MakeFundingTxResponse, parentCandidates: Set[Transaction], data: DATA_WAIT_FOR_FUNDING_INTERNAL) extends Data final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, channelFlags: Byte, lastSent: AcceptChannel) extends Data final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 14e45985..e2a48d73 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, sha256} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.{OutPoint, _} -import fr.acinq.eclair.blockchain.MakeFundingTxResponse +import fr.acinq.eclair.blockchain.wallet.EclairWallet import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions._ @@ -12,6 +12,7 @@ import fr.acinq.eclair.wire.{ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, NodeParams} import grizzled.slf4j.Logging +import scala.concurrent.Await import scala.util.{Failure, Success, Try} /** @@ -31,7 +32,6 @@ object Helpers { case d: DATA_WAIT_FOR_OPEN_CHANNEL => d.initFundee.temporaryChannelId case d: DATA_WAIT_FOR_ACCEPT_CHANNEL => d.initFunder.temporaryChannelId case d: DATA_WAIT_FOR_FUNDING_INTERNAL => d.temporaryChannelId - case d: DATA_WAIT_FOR_FUNDING_PARENT => d.data.temporaryChannelId case d: DATA_WAIT_FOR_FUNDING_CREATED => d.temporaryChannelId case d: DATA_WAIT_FOR_FUNDING_SIGNED => d.channelId case d: HasCommitments => d.channelId @@ -73,6 +73,16 @@ object Helpers { remoteFeeratePerKw > 0 && feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio } + def getFinalScriptPubKey(wallet: EclairWallet): BinaryData = { + import scala.concurrent.duration._ + val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds) + val finalScriptPubKey = Base58Check.decode(finalAddress) match { + case (Base58.Prefix.PubkeyAddressTestnet, hash) => Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) + case (Base58.Prefix.ScriptAddressTestnet, hash) => Script.write(OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUAL :: Nil) + } + finalScriptPubKey + } + object Funding { def makeFundingInputInfo(fundingTxId: BinaryData, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = { @@ -120,48 +130,6 @@ object Helpers { (localSpec, localCommitTx, remoteSpec, remoteCommitTx) } - /** - * - * @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: MakeFundingTxResponse, newParentTx: Transaction): MakeFundingTxResponse = { - // find the output that we are spending from - val utxo = newParentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt) - - // 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 - Helpers.Funding.sign(MakeFundingTxResponse(newParentTx, unsignedFundingTx, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.priv)) - } - - /** - * - * @param fundingTxResponse a funding tx response - * @return an updated funding tx response that is properly sign - */ - def sign(fundingTxResponse: MakeFundingTxResponse): MakeFundingTxResponse = { - // find the output that we are spending from - val utxo = fundingTxResponse.parentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt) - - 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) - } - } object Closing extends Logging { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 9b79f0f4..88618a67 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -6,6 +6,7 @@ import akka.actor.{ActorRef, LoggingFSM, OneForOneStrategy, PoisonPill, Props, S import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet} import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.wallet.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler.{HandshakeCompleted, Listener} import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection} @@ -43,7 +44,7 @@ case object CONNECTED extends State /** * Created by PM on 26/08/2016. */ -class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, storedChannels: Set[HasCommitments]) extends LoggingFSM[State, Data] { +class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, storedChannels: Set[HasCommitments]) extends LoggingFSM[State, Data] { import Peer._ @@ -231,13 +232,14 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[ } def createChannel(nodeParams: NodeParams, transport: ActorRef, funder: Boolean, fundingSatoshis: Long): (ActorRef, LocalParams) = { - val localParams = makeChannelParams(nodeParams, funder, fundingSatoshis) + val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet) + val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingSatoshis) val channel = spawnChannel(nodeParams, transport) (channel, localParams) } def spawnChannel(nodeParams: NodeParams, transport: ActorRef): ActorRef = { - val channel = context.actorOf(Channel.props(nodeParams, remoteNodeId, watcher, router, relayer)) + val channel = context.actorOf(Channel.props(nodeParams, wallet, remoteNodeId, watcher, router, relayer)) context watch channel channel } @@ -253,11 +255,11 @@ object Peer { val CHANNELID_ZERO = BinaryData("00" * 32) - def props(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, storedChannels: Set[HasCommitments]) = Props(new Peer(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, storedChannels)) + def props(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, storedChannels: Set[HasCommitments]) = Props(new Peer(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, wallet: EclairWallet, storedChannels)) def generateKey(nodeParams: NodeParams, keyPath: Seq[Long]): PrivateKey = DeterministicWallet.derivePrivateKey(nodeParams.extendedPrivateKey, keyPath).privateKey - def makeChannelParams(nodeParams: NodeParams, isFunder: Boolean, fundingSatoshis: Long): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: BinaryData, isFunder: Boolean, fundingSatoshis: Long): LocalParams = { // all secrets are generated from the main seed // TODO: check this val keyIndex = secureRandom.nextInt(1000).toLong @@ -273,7 +275,7 @@ object Peer { revocationSecret = generateKey(nodeParams, keyIndex :: 1L :: Nil), paymentKey = generateKey(nodeParams, keyIndex :: 2L :: Nil), delayedPaymentKey = generateKey(nodeParams, keyIndex :: 3L :: Nil), - defaultFinalScriptPubKey = nodeParams.defaultFinalScriptPubKey, + defaultFinalScriptPubKey = defaultFinalScriptPubKey, shaSeed = Crypto.sha256(generateKey(nodeParams, keyIndex :: 4L :: Nil).toBin), // TODO: check that isFunder = isFunder, globalFeatures = nodeParams.globalFeatures, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index f85d3f0f..1d97de65 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -6,6 +6,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Stat import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{MilliSatoshi, Satoshi} import fr.acinq.eclair.NodeParams +import fr.acinq.eclair.blockchain.wallet.EclairWallet import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted import fr.acinq.eclair.router.Rebroadcast @@ -14,7 +15,7 @@ import fr.acinq.eclair.router.Rebroadcast * Ties network connections to peers. * Created by PM on 14/02/2017. */ -class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef) extends Actor with ActorLogging { +class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends Actor with ActorLogging { import Switchboard._ @@ -85,7 +86,7 @@ class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, r peers.get(remoteNodeId) match { case Some(peer) => peer case None => - val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, offlineChannels), name = s"peer-$remoteNodeId") + val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, wallet, offlineChannels), name = s"peer-$remoteNodeId") context watch (peer) peer } @@ -97,7 +98,7 @@ class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, r object Switchboard { - def props(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef) = Props(new Switchboard(nodeParams, watcher, router, relayer)) + def props(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Switchboard(nodeParams, watcher, router, relayer, wallet)) // @formatter:off case class NewChannel(fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelFlags: Option[Byte]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index 3f3be420..588071f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -58,10 +58,10 @@ package object eclair { /** * Converts fee-rate-per-kB to fee-rate-per-kw, *based on a standard commit tx* * - * @param feeratePerKB + * @param feeratePerKb * @return */ - def feerateKB2Kw(feeratePerKB: Long): Long = feeratePerKB / 2 + def feerateKb2Kw(feeratePerKb: Long): Long = feeratePerKb / 2 } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index c4c67fa7..db778219 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -218,6 +218,9 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi log.warning(s"extracted paymentHash160=$paymentHash160 from tx=${Transaction.write(tx)} (claim-htlc-timeout)") paymentHash160 } + // TODO: should we handle local htlcs here as well? currently timed out htlcs that we sent will never have an answer + // TODO: we do not handle the case where htlcs transactions end up beeing unconfirmed this can happen if an htlc-success + // tx is published right before a htlc timed out val htlcsOut = bindings.collect { case b@(htlcOut, Relayed(upstream, htlcIn)) if htlcIn.paymentHash == sha256(extracted) => log.warning(s"found a match between preimage=$extracted and origin htlc=$htlcIn") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index abf2138f..69904888 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -278,24 +278,6 @@ object Transactions { def makeHtlcPenaltyTx(commitTx: Transaction): HtlcPenaltyTx = ??? - /** - * This generates a partial transaction that will be completed by bitcoind using a 'fundrawtransaction' rpc call. - * Since bitcoind may add a change output, we return the pubkeyScript so that we can do a lookup afterwards. - * - * @param amount - * @param localFundingPubkey - * @param remoteFundingPubkey - * @return (partialTx, pubkeyScript) - */ - def makePartialFundingTx(amount: Satoshi, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey): (Transaction, BinaryData) = { - val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteFundingPubkey))) - (Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = TxOut(amount, pubkeyScript) :: Nil, - lockTime = 0), pubkeyScript) - } - def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: BinaryData, remoteScriptPubKey: BinaryData, localIsFunder: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { require(spec.htlcs.size == 0, "there shouldn't be any pending htlcs") @@ -381,6 +363,9 @@ object Transactions { closingTx.copy(tx = closingTx.tx.updateWitness(0, witness)) } + def checkSpendable(parent: Transaction, child: Transaction): Try[Unit] = + Try(Transaction.correctlySpends(child, parent :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] = Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.tx.txIn(0).outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) diff --git a/eclair-core/src/test/resources/logback-test.xml b/eclair-core/src/test/resources/logback-test.xml index 06a4821f..98a65c05 100644 --- a/eclair-core/src/test/resources/logback-test.xml +++ b/eclair-core/src/test/resources/logback-test.xml @@ -26,6 +26,8 @@ + + diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala index 2c70cfd0..d2555b4f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala @@ -1,16 +1,12 @@ package fr.acinq.eclair import akka.actor.ActorSystem -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{BinaryData, Block, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain.ExtendedBitcoinClient.SignTransactionResponse -import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient +import fr.acinq.bitcoin.{Block, Transaction} import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try /** * Created by PM on 26/04/2016. @@ -25,9 +21,6 @@ class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinC override def run(): Unit = system.eventStream.publish(NewBlock(DUMMY_BLOCK)) // blocks are not actually interpreted }) - override def makeFundingTx(ourCommitPub: PublicKey, theirCommitPub: PublicKey, amount: Satoshi, feeRatePerKw: Long)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = - Future.successful(TestBitcoinClient.makeDummyFundingTx(MakeFundingTx(ourCommitPub, theirCommitPub, amount, feeRatePerKw))) - override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = { system.eventStream.publish(NewTransaction(tx)) Future.successful(tx.txid.toString()) @@ -37,38 +30,6 @@ class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinC override def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] = ??? - override def fundTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ExtendedBitcoinClient.FundTransactionResponse] = ??? - - override def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = ??? - override def getTransactionShortId(txId: String)(implicit ec: ExecutionContext): Future[(Int, Int)] = Future.successful((400000, 42)) -} - -object TestBitcoinClient { - - def makeDummyFundingTx(makeFundingTx: MakeFundingTx): MakeFundingTxResponse = { - val priv = PrivateKey(BinaryData("01" * 32), compressed = true) - val parentTx = Transaction(version = 2, - txIn = TxIn(OutPoint("42" * 32, 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(makeFundingTx.amount, Script.pay2sh(Script.pay2wpkh(priv.publicKey))) :: Nil, - lockTime = 0) - val anchorTx = Transaction(version = 2, - txIn = TxIn(OutPoint(parentTx, 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(makeFundingTx.amount, Script.pay2wsh(Scripts.multiSig2of2(makeFundingTx.localCommitPub, makeFundingTx.remoteCommitPub))) :: Nil, - lockTime = 0) - MakeFundingTxResponse(parentTx, anchorTx, 0, priv) - } - - def malleateTx(tx: Transaction): Transaction = { - val inputs1 = tx.txIn.map(input => Script.parse(input.signatureScript) match { - case OP_PUSHDATA(sig, _) :: OP_PUSHDATA(pub, _) :: Nil if pub.length == 33 && Try(Crypto.decodeSignature(sig)).isSuccess => - val (r, s) = Crypto.decodeSignature(sig) - val s1 = Crypto.curve.getN.subtract(s) - val sig1 = Crypto.encodeSignature(r, s1) - input.copy(signatureScript = Script.write(OP_PUSHDATA(sig1) :: OP_PUSHDATA(pub) :: Nil)) - }) - val tx1 = tx.copy(txIn = inputs1) - tx1 - } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 2fabcb21..8dddfffc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -43,7 +43,6 @@ object TestConstants { feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overriden below) maxReserveToFundingRatio = 0.05, - defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(5), compressed = true).publicKey)), channelsDb = new SqliteChannelsDb(sqlite), peersDb = new SqlitePeersDb(sqlite), networkDb = new SqliteNetworkDb(sqlite), @@ -54,10 +53,12 @@ object TestConstants { updateFeeMinDiffRatio = 0.1, autoReconnect = false, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = 1) + channelFlags = 1, + spv = false) def id = nodeParams.privateKey.publicKey def channelParams = Peer.makeChannelParams( nodeParams = nodeParams, + defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(4), compressed = true).publicKey)), isFunder = true, fundingSatoshis).copy( channelReserveSatoshis = 10000 // Bob will need to keep that much satoshis as direct payment @@ -89,7 +90,6 @@ object TestConstants { feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overriden below) maxReserveToFundingRatio = 0.05, - defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(5), compressed = true).publicKey)), channelsDb = new SqliteChannelsDb(sqlite), peersDb = new SqlitePeersDb(sqlite), networkDb = new SqliteNetworkDb(sqlite), @@ -100,10 +100,12 @@ object TestConstants { updateFeeMinDiffRatio = 0.1, autoReconnect = false, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = 1) + channelFlags = 1, + spv = false) def id = nodeParams.privateKey.publicKey def channelParams = Peer.makeChannelParams( nodeParams = nodeParams, + defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(5), compressed = true).publicKey)), isFunder = false, fundingSatoshis).copy( channelReserveSatoshis = 20000 // Alice will need to keep that much satoshis as direct payment diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/BitcoinjSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/BitcoinjSpec.scala new file mode 100644 index 00000000..d31adf86 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/BitcoinjSpec.scala @@ -0,0 +1,220 @@ +package fr.acinq.eclair.blockchain + +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.rpc.BitcoinJsonRPCClient +import fr.acinq.eclair.blockchain.spv.BitcoinjKit +import fr.acinq.eclair.blockchain.wallet.BitcoinjWallet +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.core.{Coin, Transaction} +import org.bitcoinj.script.{Script => BitcoinjScript} +import org.bitcoinj.wallet.{SendRequest, Wallet} +import org.json4s.DefaultFormats +import org.json4s.JsonAST.JValue +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 + +@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 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](10 seconds) + } + + ignore("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? + }*/ + + ignore("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 SpvWatcher(bitcoinjKit)), name = "spv-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) + watcher ! Hint(new BitcoinjScript(fundingPubkeyScript)) + assert(Await.result(wallet.commit(result.fundingTx), 10 seconds)) + watcher ! WatchSpent(listener.ref, result.fundingTx.txid, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT) + watcher ! WatchConfirmed(listener.ref, result.fundingTx.txid, 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) + } + + ignore("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 SpvWatcher(bitcoinjKit)), name = "spv-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) + watcher ! Hint(new BitcoinjScript(fundingPubkeyScript)) + assert(Await.result(wallet.commit(result.fundingTx), 10 seconds)) + watcher ! WatchSpent(listener.ref, result.fundingTx.txid, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT) + watcher ! WatchConfirmed(listener.ref, result.fundingTx.txid, 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/blockchain/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClientSpec.scala index 29d66d79..e03dd7af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClientSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.blockchain import akka.actor.ActorSystem import com.typesafe.config.ConfigFactory -import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient +import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} import org.scalatest.FunSuite import scala.concurrent.{Await, ExecutionContext} @@ -22,7 +22,7 @@ class ExtendedBitcoinClientSpec extends FunSuite { implicit val formats = org.json4s.DefaultFormats implicit val ec = ExecutionContext.Implicits.global - val (chain, blockCount) = Await.result(client.client.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long])), 10 seconds) + val (chain, blockCount) = Await.result(client.rpcClient.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long])), 10 seconds) assert(chain == "test", "you should be on testnet") test("get transaction short id") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/PeerWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/PeerWatcherSpec.scala deleted file mode 100644 index 410916b9..00000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/PeerWatcherSpec.scala +++ /dev/null @@ -1,81 +0,0 @@ -package fr.acinq.eclair.blockchain - -import akka.actor.ActorSystem -import akka.testkit.TestKit -import akka.util.Timeout -import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin.SigVersion._ -import fr.acinq.bitcoin._ -import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient -import fr.acinq.eclair.transactions.Scripts._ -import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.{TestConstants, randomKey} -import org.junit.runner.RunWith -import org.scalatest.FunSuiteLike -import org.scalatest.junit.JUnitRunner - -import scala.concurrent.Await -import scala.concurrent.duration._ - -/** - * Created by PM on 22/02/2017. - */ -@RunWith(classOf[JUnitRunner]) -class PeerWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { - - ignore("publish a csv tx") { - import scala.concurrent.ExecutionContext.Implicits.global - implicit val formats = org.json4s.DefaultFormats - implicit val timeout = Timeout(30 seconds) - - val bitcoin_client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient("foo", "bar", port = 18332)) - val (chain, blockCount, progress) = Await.result(bitcoin_client.client.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long], (json \ "verificationprogress").extract[Double])), 10 seconds) - assert(chain == "regtest") - - val watcher = system.actorOf(PeerWatcher.props(TestConstants.Alice.nodeParams, bitcoin_client)) - - // first we pick a random key - val localDelayedKey = randomKey - val revocationKey = randomKey - // then a delayed script - val delay = 10 - val redeemScript = write(toLocalDelayed(revocationKey.publicKey, delay, localDelayedKey.publicKey)) - val pubKeyScript = write(pay2wsh(redeemScript)) - // and we generate a tx which pays to the delayed script - val amount = Satoshi(1000000) - val partialParentTx = Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = TxOut(amount, pubKeyScript) :: Nil, - lockTime = 0) - // we ask bitcoind to fund the tx - val futureParentTx = for { - funded <- bitcoin_client.fundTransaction(partialParentTx).map(_.tx) - signed <- bitcoin_client.signTransaction(funded) - } yield signed.tx - val parentTx = Await.result(futureParentTx, 10 seconds) - val outputIndex = Transactions.findPubKeyScriptIndex(parentTx, pubKeyScript) - // we build a tx spending the parent tx - val finalPubKeyHash = Base58Check.decode("mkmJFtGN5QvVyYz2NLXPGW1p2SABo2LV9y")._2 - val unsignedTx = Transaction( - version = 2, - txIn = TxIn(OutPoint(parentTx.hash, outputIndex), Array.emptyByteArray, delay) :: Nil, - txOut = TxOut(Satoshi(900000), Script.pay2pkh(finalPubKeyHash)) :: Nil, - lockTime = 0) - val sig = Transaction.signInput(unsignedTx, 0, redeemScript, SIGHASH_ALL, amount, SIGVERSION_WITNESS_V0, localDelayedKey) - val witness = witnessToLocalDelayedAfterDelay(sig, redeemScript) - val tx = unsignedTx.updateWitness(0, witness) - - watcher ! NewBlock(Block(null, Nil)) - watcher ! PublishAsap(tx) - Thread.sleep(5000) - watcher ! PublishAsap(parentTx) - // tester should manually generate blocks - while(true) { - Thread.sleep(5000) - watcher ! NewBlock(Block(null, Nil)) - } - - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala new file mode 100644 index 00000000..4792cd8a --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala @@ -0,0 +1,45 @@ +package fr.acinq.eclair.blockchain + +import fr.acinq.bitcoin.{BinaryData, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.eclair.blockchain.wallet.{EclairWallet, MakeFundingTxResponse} + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.Try + +/** + * Created by PM on 06/07/2017. + */ +class TestWallet extends EclairWallet { + + override def getBalance: Future[Satoshi] = ??? + + override def getFinalAddress: Future[String] = Future.successful("2MsRZ1asG6k94m6GYUufDGaZJMoJ4EV5JKs") + + override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = + Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount, feeRatePerKw)) + + override def commit(tx: Transaction): Future[Boolean] = Future.successful(true) +} + +object TestWallet { + + def makeDummyFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): MakeFundingTxResponse = { + val fundingTx = Transaction(version = 2, + txIn = TxIn(OutPoint("42" * 32, 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, + txOut = TxOut(amount, pubkeyScript) :: Nil, + lockTime = 0) + MakeFundingTxResponse(fundingTx, 0) + } + + def malleateTx(tx: Transaction): Transaction = { + val inputs1 = tx.txIn.map(input => Script.parse(input.signatureScript) match { + case OP_PUSHDATA(sig, _) :: OP_PUSHDATA(pub, _) :: Nil if pub.length == 33 && Try(Crypto.decodeSignature(sig)).isSuccess => + val (r, s) = Crypto.decodeSignature(sig) + val s1 = Crypto.curve.getN.subtract(s) + val sig1 = Crypto.encodeSignature(r, s1) + input.copy(signatureScript = Script.write(OP_PUSHDATA(sig1) :: OP_PUSHDATA(pub) :: Nil)) + }) + val tx1 = tx.copy(txIn = inputs1) + tx1 + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index 017c34ec..6e0b045a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -22,7 +22,7 @@ class ThroughputSpec extends FunSuite { ignore("throughput") { implicit val system = ActorSystem() val pipe = system.actorOf(Props[Pipe], "pipe") - val blockchain = system.actorOf(PeerWatcher.props(TestConstants.Alice.nodeParams, new TestBitcoinClient()), "blockchain") + val blockchain = system.actorOf(ZmqWatcher.props(new TestBitcoinClient()), "blockchain") val paymentHandler = system.actorOf(Props(new Actor() { val random = new Random() @@ -54,8 +54,9 @@ class ThroughputSpec extends FunSuite { }), "payment-handler") val relayerA = system.actorOf(Relayer.props(Alice.nodeParams.privateKey, paymentHandler)) val relayerB = system.actorOf(Relayer.props(Bob.nodeParams.privateKey, paymentHandler)) - val alice = system.actorOf(Channel.props(Alice.nodeParams, Bob.id, blockchain, ???, relayerA), "a") - val bob = system.actorOf(Channel.props(Bob.nodeParams, Alice.id, blockchain, ???, relayerB), "b") + val wallet = new TestWallet + val alice = system.actorOf(Channel.props(Alice.nodeParams, wallet, Bob.id, blockchain, ???, relayerA), "a") + val bob = system.actorOf(Channel.props(Bob.nodeParams, wallet, Alice.id, blockchain, ???, relayerB), "b") val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/FuzzySpec.scala index 0e83b7f9..0d65ef6f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/FuzzySpec.scala @@ -38,8 +38,9 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi val relayerA = system.actorOf(Relayer.props(Alice.nodeParams.privateKey, paymentHandlerA), "relayer-a") val relayerB = system.actorOf(Relayer.props(Bob.nodeParams.privateKey, paymentHandlerB), "relayer-b") val router = TestProbe() - val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, Bob.id, alice2blockchain.ref, router.ref, relayerA)) - val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, Alice.id, bob2blockchain.ref, router.ref, relayerB)) + val wallet = new TestWallet + val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.id, alice2blockchain.ref, router.ref, relayerA)) + val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, wallet, Alice.id, bob2blockchain.ref, router.ref, relayerB)) within(30 seconds) { val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) @@ -49,14 +50,6 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte) bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) - val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) - alice ! dummyFundingTx - val w = alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx) - alice2blockchain.expectMsgType[WatchConfirmed] - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42) alice2blockchain.expectMsgType[WatchSpent] alice2blockchain.expectMsgType[WatchConfirmed] alice2blockchain.expectMsgType[PublishAsap] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 4427a30b..a8572087 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -1,17 +1,15 @@ package fr.acinq.eclair.channel.states import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{BinaryData, Crypto, OutPoint, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{BinaryData, Crypto} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentLifecycle import fr.acinq.eclair.router.Hop -import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, TestBitcoinClient, TestConstants} +import fr.acinq.eclair.{Globals, TestConstants} import scala.util.Random @@ -41,8 +39,9 @@ trait StateTestsHelperMethods extends TestKitBase { val router = TestProbe() val nodeParamsA = TestConstants.Alice.nodeParams val nodeParamsB = TestConstants.Bob.nodeParams - val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, Bob.id, alice2blockchain.ref, router.ref, relayer.ref)) - val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, Alice.id, bob2blockchain.ref, router.ref, relayer.ref)) + val wallet = new TestWallet + val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, wallet, Bob.id, alice2blockchain.ref, router.ref, relayer.ref)) + val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, wallet, Alice.id, bob2blockchain.ref, router.ref, relayer.ref)) Setup(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayer) } @@ -64,14 +63,6 @@ trait StateTestsHelperMethods extends TestKitBase { alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) - alice ! dummyFundingTx - val w = alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx) - alice2blockchain.expectMsgType[WatchConfirmed] - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42) alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) bob2alice.expectMsgType[FundingSigned] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 5f44b061..03c5b05b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -1,13 +1,11 @@ package fr.acinq.eclair.channel.states.b -import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -33,21 +31,19 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) } test((alice, alice2bob, bob2alice, alice2blockchain)) } - test("recv funding transaction") { case (alice, alice2bob, bob2alice, alice2blockchain) => + /*test("recv MakeFundingTxResponse") { case (alice, alice2bob, bob2alice, alice2blockchain) => within(30 seconds) { val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) + val dummyFundingTx = TestWallet.makeDummyFundingTx(makeFundingTx) alice ! dummyFundingTx - val w = alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_PARENT) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED) } - } + }*/ test("recv Error") { case (bob, alice2bob, bob2alice, _) => within(30 seconds) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 8ad9da59..d7712dfb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -8,7 +8,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.junit.runner.RunWith import org.scalatest.Tag import org.scalatest.junit.JUnitRunner @@ -41,14 +41,6 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) - alice ! dummyFundingTx - val w = alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx) - alice2blockchain.expectMsgType[WatchConfirmed] - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) } test((bob, alice2bob, bob2alice, bob2blockchain)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingParentStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingParentStateSpec.scala deleted file mode 100644 index b3de8930..00000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingParentStateSpec.scala +++ /dev/null @@ -1,70 +0,0 @@ -package fr.acinq.eclair.channel.states.b - -import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Transaction -import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner - -import scala.concurrent.duration._ - -/** - * Created by PM on 05/07/2016. - */ -@RunWith(classOf[JUnitRunner]) -class WaitForFundingParentStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - - type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, Transaction] - - override def withFixture(test: OneArgTest) = { - val setup = init() - import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) - within(30 seconds) { - alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) - bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, bob2alice.ref, aliceInit) - alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) - bob2alice.expectMsgType[AcceptChannel] - bob2alice.forward(alice) - val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) - alice ! dummyFundingTx - alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_PARENT) - test((alice, alice2bob, bob2alice, alice2blockchain, dummyFundingTx.parentTx)) - } - } - - test("recv BITCOIN_INPUT_SPENT and then BITCOIN_TX_CONFIRMED") { case (alice, alice2bob, _, alice2blockchain, parentTx) => - within(30 seconds) { - alice ! WatchEventSpent(BITCOIN_INPUT_SPENT(parentTx), parentTx) - alice2blockchain.expectMsgType[WatchConfirmed] - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(parentTx), 400000, 42) - alice2bob.expectMsgType[FundingCreated] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED) - } - } - - test("recv Error") { case (bob, alice2bob, bob2alice, _, _) => - within(30 seconds) { - bob ! Error("00" * 32, "oops".getBytes) - awaitCond(bob.stateName == CLOSED) - } - } - - test("recv CMD_CLOSE") { case (alice, alice2bob, bob2alice, _, _) => - within(30 seconds) { - alice ! CMD_CLOSE(None) - awaitCond(alice.stateName == CLOSED) - } - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index aaae18aa..eed1e7d7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -1,6 +1,5 @@ package fr.acinq.eclair.channel.states.b -import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.TestConstants.{Alice, Bob} @@ -8,7 +7,7 @@ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel} -import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -34,14 +33,6 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) - alice ! dummyFundingTx - val w = alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx) - alice2blockchain.expectMsgType[WatchConfirmed] - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42) alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 559ababc..cbf534c6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -1,13 +1,12 @@ package fr.acinq.eclair.channel.states.c -import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} -import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -33,14 +32,6 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) - alice ! dummyFundingTx - val w = alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx) - alice2blockchain.expectMsgType[WatchConfirmed] - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42) alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) bob2alice.expectMsgType[FundingSigned] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 782bfe88..9b178ee3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -1,13 +1,12 @@ package fr.acinq.eclair.channel.states.c -import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -33,14 +32,6 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx] - val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx) - alice ! dummyFundingTx - val w = alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[PublishAsap] - alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx) - alice2blockchain.expectMsgType[WatchConfirmed] - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42) alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) bob2alice.expectMsgType[FundingSigned] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BasicIntegrationSpvSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BasicIntegrationSpvSpec.scala new file mode 100644 index 00000000..0b7f0aca --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BasicIntegrationSpvSpec.scala @@ -0,0 +1,653 @@ +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.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.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} +import fr.acinq.eclair.blockchain.wallet.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.Disconnect +import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection} +import fr.acinq.eclair.payment.{State => _, _} +import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec} +import fr.acinq.eclair.wire._ +import fr.acinq.eclair.{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 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 + 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.left.get.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() + 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}") + 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(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(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(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(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(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(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-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 1e082dca..82bbbe21 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 @@ -8,10 +8,10 @@ import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, Satoshi, Script} -import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient -import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, Watch, WatchConfirmed} +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} +import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} +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 @@ -49,7 +49,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit var bitcoinrpcclient: BitcoinJsonRPCClient = null var bitcoincli: ActorRef = null var nodes: Map[String, Kit] = Map() - var finalAddresses: Map[String, String] = Map() implicit val formats = DefaultFormats @@ -107,7 +106,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit } val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name")) val kit = Await.result(setup.bootstrap, 10 seconds) - finalAddresses = finalAddresses + (name -> setup.finalAddress) nodes = nodes + (name -> kit) } @@ -119,7 +117,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit test("starting eclair nodes") { import collection.JavaConversions._ - val commonConfig = ConfigFactory.parseMap(Map("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 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)) 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)) @@ -145,7 +143,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit // funder transitions assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_ACCEPT_CHANNEL) assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_INTERNAL) - assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_PARENT) // fundee transitions assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_OPEN_CHANNEL) assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CREATED) @@ -363,16 +360,23 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 _ => ??? + } + test("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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test - sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) - val res = sender.expectMsgType[JValue](10 seconds) - val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) // 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 @@ -385,6 +389,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) + val finalAddressF = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + // we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test + sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) + val res = sender.expectMsgType[JValue](10 seconds) + val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) // we then kill the connection between C and F sender.send(nodes("F1").switchboard, 'peers) val peers = sender.expectMsgType[Map[PublicKey, ActorRef]] @@ -412,7 +425,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) val res = sender.expectMsgType[JValue](10 seconds) - res.filter(_ \ "address" == JString(finalAddresses("F1"))).flatMap(_ \ "txids" \\ classOf[JString]).size == 1 + res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1 }, max = 30 seconds, interval = 1 second) // we then generate enough blocks so that C gets its main delayed output sender.send(bitcoincli, BitcoinReq("generate", 145)) @@ -421,7 +434,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) val res = sender.expectMsgType[JValue](10 seconds) - val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) + val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) (receivedByC diff previouslyReceivedByC).size == 1 }, max = 30 seconds, interval = 1 second) awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16) @@ -433,10 +446,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test - sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) - val res = sender.expectMsgType[JValue](10 seconds) - val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) // 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 @@ -449,6 +458,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATEDATA)) + val finalAddressF = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + // we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test + sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) + val res = sender.expectMsgType[JValue](10 seconds) + val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) // we then kill the connection between C and F sender.send(nodes("F2").switchboard, 'peers) val peers = sender.expectMsgType[Map[PublicKey, ActorRef]] @@ -475,13 +493,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) val res = sender.expectMsgType[JValue](10 seconds) - res.filter(_ \ "address" == JString(finalAddresses("F2"))).flatMap(_ \ "txids" \\ classOf[JString]).size == 1 + res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1 }, max = 30 seconds, interval = 1 second) // and C will have its main output awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) val res = sender.expectMsgType[JValue](10 seconds) - val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) + val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) (receivedByC diff previouslyReceivedByC).size == 1 }, max = 30 seconds, interval = 1 second) awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14) @@ -493,10 +511,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test - sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) - val res = sender.expectMsgType[JValue](10 seconds) - val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) // 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 @@ -509,6 +523,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + // we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test + sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) + val res = sender.expectMsgType[JValue](10 seconds) + val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) // we then generate enough blocks to make the htlc timeout sender.send(bitcoincli, BitcoinReq("generate", 11)) sender.expectMsgType[JValue](10 seconds) @@ -524,7 +545,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) val res = sender.expectMsgType[JValue](10 seconds) - val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) + val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) (receivedByC diff previouslyReceivedByC).size == 2 }, max = 30 seconds, interval = 1 second) awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12) @@ -536,10 +557,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test - sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) - val res = sender.expectMsgType[JValue](10 seconds) - val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) // 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 @@ -552,6 +569,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit 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 finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + // we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test + sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) + val res = sender.expectMsgType[JValue](10 seconds) + val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) // 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 @@ -569,7 +593,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) val res = sender.expectMsgType[JValue](10 seconds) - val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString]) + val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) (receivedByC diff previouslyReceivedByC).size == 2 }, max = 30 seconds, interval = 1 second) awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 3930c08c..e5dd1a17 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -5,12 +5,12 @@ import java.util.concurrent.{CountDownLatch, TimeUnit} import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import fr.acinq.eclair.Globals import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain.PeerWatcher +import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.NoopPaymentHandler import fr.acinq.eclair.wire.Init -import fr.acinq.eclair.{Globals, TestBitcoinClient, TestConstants} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, Matchers, fixture} @@ -30,14 +30,15 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix Globals.blockCount.set(0) val latch = new CountDownLatch(1) val pipe: ActorRef = system.actorOf(Props(new SynchronizationPipe(latch))) - val blockchainA = system.actorOf(PeerWatcher.props(TestConstants.Alice.nodeParams, new TestBitcoinClient())) - val blockchainB = system.actorOf(PeerWatcher.props(TestConstants.Bob.nodeParams, new TestBitcoinClient())) + val alice2blockchain = TestProbe() + val bob2blockchain = TestProbe() val paymentHandler = system.actorOf(Props(new NoopPaymentHandler())) // we just bypass the relayer for this test val relayer = paymentHandler val router = TestProbe() - val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, Bob.id, blockchainA, router.ref, relayer)) - val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, Alice.id, blockchainB, router.ref, relayer)) + val wallet = new TestWallet + val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.id, alice2blockchain.ref, router.ref, relayer)) + val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, wallet, Alice.id, bob2blockchain.ref, router.ref, relayer)) val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) // alice and bob will both have 1 000 000 sat @@ -46,6 +47,15 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) within(30 seconds) { + alice2blockchain.expectMsgType[WatchSpent] + alice2blockchain.expectMsgType[WatchConfirmed] + alice2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchSpent] + bob2blockchain.expectMsgType[WatchConfirmed] + alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42) + bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42) + alice2blockchain.expectMsgType[WatchLost] + bob2blockchain.expectMsgType[WatchLost] awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) } 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 74cf0480..fa2d6417 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 @@ -2,9 +2,10 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Transaction} -import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, MakeFundingTxResponse} -import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient +import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction} +import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient} +import fr.acinq.eclair.blockchain.wallet.BitcoinCoreWallet +import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.ChannelAnnouncement import fr.acinq.eclair.{randomKey, toShortId} import org.junit.runner.RunWith @@ -56,20 +57,22 @@ object AnnouncementsBatchValidationSpec { case class SimulatedChannel(node1Key: PrivateKey, node2Key: PrivateKey, node1FundingKey: PrivateKey, node2FundingKey: PrivateKey, amount: Satoshi, fundingTx: Transaction, fundingOutputIndex: Int) def generateBlocks(numBlocks: Int)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext) = - Await.result(extendedBitcoinClient.client.invoke("generate", numBlocks), 10 seconds) + Await.result(extendedBitcoinClient.rpcClient.invoke("generate", numBlocks), 10 seconds) - def simulateChannel()(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): SimulatedChannel = { + def simulateChannel()(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext, system: ActorSystem): SimulatedChannel = { val node1Key = randomKey val node2Key = randomKey val node1BitcoinKey = randomKey val node2BitcoinKey = randomKey val amount = Satoshi(1000000) // first we publish the funding tx - val fundingTxFuture = extendedBitcoinClient.makeFundingTx(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey, amount, 10000) - val MakeFundingTxResponse(parentTx, fundingTx, fundingOutputIndex, _) = Await.result(fundingTxFuture, 10 seconds) - Await.result(extendedBitcoinClient.publishTransaction(parentTx), 10 seconds) - Await.result(extendedBitcoinClient.publishTransaction(fundingTx), 10 seconds) - SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, fundingTx, fundingOutputIndex) + val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient, null) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey))) + val fundingTxFuture = wallet.makeParentAndFundingTx(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) } def makeChannelAnnouncement(c: SimulatedChannel)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): ChannelAnnouncement = { diff --git a/eclair-node-gui/pom.xml b/eclair-node-gui/pom.xml index 0f69bce0..0ac57f38 100644 --- a/eclair-node-gui/pom.xml +++ b/eclair-node-gui/pom.xml @@ -5,7 +5,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-spv-SNAPSHOT eclair-node-gui_2.11 diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala index 80cab07c..68dc1c15 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala @@ -31,78 +31,69 @@ class FxApp extends Application with Logging { logger.debug("initializing application...") } + def onError(t: Throwable): Unit = t match { + case TCPBindException(port) => + notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null)) + case BitcoinRPCConnectionException => + notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using JSON-RPC.", null)) + notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and RPC parameters are correct.")) + case BitcoinZMQConnectionTimeoutException => + notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using ZMQ.", null)) + notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and ZMQ parameters are correct.")) + case IncompatibleDBException => + notifyPreloader(new ErrorNotification("Setup", "Breaking changes!", null)) + notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible.")) + notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir.")) + case t: Throwable => + notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t)) + } + override def start(primaryStage: Stage): Unit = { - val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false) - primaryStage.getIcons.add(icon) - - def onError(t: Throwable): Unit = t match { - case TCPBindException(port) => - notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null)) - case BitcoinRPCConnectionException => - notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using JSON-RPC.", null)) - notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and RPC parameters are correct.")) - case BitcoinZMQConnectionTimeoutException => - notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using ZMQ.", null)) - notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and ZMQ parameters are correct.")) - case IncompatibleDBException => - notifyPreloader(new ErrorNotification("Setup", "Breaking changes!", null)) - notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible.")) - notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir.")) - case t: Throwable => - notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t)) - } - new Thread(new Runnable { override def run(): Unit = { try { + val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false) + primaryStage.getIcons.add(icon) + val mainFXML = new FXMLLoader(getClass.getResource("/gui/main/main.fxml")) + val pKit = Promise[Kit]() + val handlers = new Handlers(pKit.future) + val controller = new MainController(handlers, getHostServices) + mainFXML.setController(controller) + val mainRoot = mainFXML.load[Parent] val datadir = new File(getParameters.getUnnamed.get(0)) implicit val system = ActorSystem("system") val setup = new Setup(datadir) - val pKit = Promise[Kit]() - val handlers = new Handlers(pKit.future) - val controller = new MainController(handlers, setup, getHostServices) val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume)) setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent]) setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent]) setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent]) setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvents]) - - Platform.runLater(new Runnable { - override def run(): Unit = { - val mainFXML = new FXMLLoader(getClass.getResource("/gui/main/main.fxml")) - mainFXML.setController(controller) - val mainRoot = mainFXML.load[Parent] - val scene = new Scene(mainRoot) - - primaryStage.setTitle("Eclair") - primaryStage.setMinWidth(600) - primaryStage.setWidth(960) - primaryStage.setMinHeight(400) - primaryStage.setHeight(640) - primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] { - override def handle(event: WindowEvent) { - System.exit(0) - } - }) - import scala.concurrent.ExecutionContext.Implicits.global - setup.bootstrap onComplete { - case Success(kit) => - Platform.runLater(new Runnable { - override def run(): Unit = { - notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful")) - primaryStage.setScene(scene) - primaryStage.show - initNotificationStage(primaryStage, handlers) + pKit.completeWith(setup.bootstrap) + import scala.concurrent.ExecutionContext.Implicits.global + pKit.future.onComplete { + case Success(_) => + Platform.runLater(new Runnable { + override def run(): Unit = { + val scene = new Scene(mainRoot) + primaryStage.setTitle("Eclair") + primaryStage.setMinWidth(600) + primaryStage.setWidth(960) + primaryStage.setMinHeight(400) + primaryStage.setHeight(640) + primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] { + override def handle(event: WindowEvent) { + System.exit(0) } }) - pKit.success(kit) - case Failure(t) => onError(t) - - } - - } - }) - + controller.initInfoFields(setup) + primaryStage.setScene(scene) + primaryStage.show + notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful")) + initNotificationStage(primaryStage, handlers) + } + }) + case Failure(t) => onError(t) + } } catch { case t: Throwable => onError(t) } 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 a1e78ba5..04768ffe 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 @@ -47,7 +47,7 @@ case class PaymentRelayedRecord(event: PaymentRelayed, date: LocalDateTime) exte /** * Created by DPA on 22/09/2016. */ -class MainController(val handlers: Handlers, val setup: Setup, val hostServices: HostServices) extends Logging { +class MainController(val handlers: Handlers, val hostServices: HostServices) extends Logging { @FXML var root: AnchorPane = _ var contextMenu: ContextMenu = _ @@ -130,23 +130,6 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices: */ @FXML def initialize = { - // init status bar - labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}") - labelAlias.setText(s"${setup.nodeParams.alias}") - rectRGB.setFill(Color.rgb(setup.nodeParams.color._1 & 0xFF, setup.nodeParams.color._2 & 0xFF, setup.nodeParams.color._3 & 0xFF)) - labelApi.setText(s"${setup.config.getInt("api.port")}") - labelServer.setText(s"${setup.config.getInt("server.port")}") - bitcoinVersion.setText(s"v${setup.bitcoinVersion}") - bitcoinChain.setText(s"${setup.chain.toUpperCase()}") - bitcoinChain.getStyleClass.add(setup.chain) - - // init context - contextMenu = ContextMenuUtils.buildCopyContext( - List( - Some(new CopyAction("Copy Pubkey", s"${setup.nodeParams.privateKey.publicKey}")), - setup.nodeParams.publicAddresses.headOption.map(address => new CopyAction("Copy URI", s"${setup.nodeParams.privateKey.publicKey}@${address.getHostString}:${address.getPort}")) - ).flatten) - // init channels tab if (channelBox.getChildren.size() > 0) { channelInfo.setScaleY(0) @@ -323,6 +306,25 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices: paymentRelayedTable.setRowFactory(paymentRowFactory) } + def initInfoFields(setup: Setup) = { + // init status bar + labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}") + labelAlias.setText(s"${setup.nodeParams.alias}") + rectRGB.setFill(Color.rgb(setup.nodeParams.color._1 & 0xFF, setup.nodeParams.color._2 & 0xFF, setup.nodeParams.color._3 & 0xFF)) + labelApi.setText(s"${setup.config.getInt("api.port")}") + labelServer.setText(s"${setup.config.getInt("server.port")}") + bitcoinVersion.setText(s"v0.0.0") + //bitcoinVersion.setText(s"v${setup.bitcoinVersion}") + bitcoinChain.setText(s"${setup.chain.toUpperCase()}") + bitcoinChain.getStyleClass.add(setup.chain) + + contextMenu = ContextMenuUtils.buildCopyContext( + List( + Some(new CopyAction("Copy Pubkey", s"${setup.nodeParams.privateKey.publicKey}")), + setup.nodeParams.publicAddresses.headOption.map(address => new CopyAction("Copy URI", s"${setup.nodeParams.privateKey.publicKey}@${address.getHostString}:${address.getPort}")) + ).flatten) + } + private def updateTabHeader(tab: Tab, prefix: String, items: ObservableList[_]) = { Platform.runLater(new Runnable() { override def run = tab.setText(s"$prefix (${items.size})") diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 15d6d01e..dd100e8c 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -5,7 +5,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-spv-SNAPSHOT eclair-node_2.11 diff --git a/eclair-node/src/main/resources/logback.xml b/eclair-node/src/main/resources/logback.xml index f436afde..95e52262 100644 --- a/eclair-node/src/main/resources/logback.xml +++ b/eclair-node/src/main/resources/logback.xml @@ -1,13 +1,13 @@ - + ${eclair.datadir:-${user.home}/.eclair}/eclair.log diff --git a/eclair-node/src/main/resources/logback_colors.xml b/eclair-node/src/main/resources/logback_colors.xml index de29a3a2..2570d43a 100644 --- a/eclair-node/src/main/resources/logback_colors.xml +++ b/eclair-node/src/main/resources/logback_colors.xml @@ -18,6 +18,15 @@ + + System.out + false + + %boldYellow(${HOSTNAME} %d) %highlight(%-5level) %logger{36} %X{akkaSource} - %yellow(%msg) %ex{12}%n + + + + System.out false @@ -75,10 +84,26 @@ - + + + + + + + + + + + + + + + + + diff --git a/eclair-node/src/test/resources/logback-test.xml b/eclair-node/src/test/resources/logback-test.xml deleted file mode 100644 index 020fb1f5..00000000 --- a/eclair-node/src/test/resources/logback-test.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - System.out - - %date{HH:mm:ss.SSS} %highlight(%-5level) %X{akkaSource} - %msg%ex{12}%n - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index c8900a63..bbc13518 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-spv-SNAPSHOT pom @@ -48,6 +48,7 @@ 2.11 2.4.18 0.9.13 + 0.15-rc2