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