diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala
index 8027f8a3c..75fa01c88 100644
--- a/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala
+++ b/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala
@@ -36,7 +36,7 @@ object Boot extends App with Logging {
assert(chain == "testnet" || chain == "regtest" || chain == "segnet4", "you should be on testnet or regtest or segnet4")
val blockchain = system.actorOf(Props(new PollingWatcher(new ExtendedBitcoinClient(bitcoin_client))), name = "blockchain")
- val register = system.actorOf(Props[Register], name = "register")
+ val register = system.actorOf(Register.props(blockchain), name = "register")
val server = system.actorOf(Server.props(config.getString("eclair.server.address"), config.getInt("eclair.server.port")), "server")
val api = new Service {
diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClient.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClient.scala
index 5d9b530b9..e6b055490 100644
--- a/eclair-demo/src/main/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClient.scala
+++ b/eclair-demo/src/main/scala/fr/acinq/eclair/blockchain/ExtendedBitcoinClient.scala
@@ -21,7 +21,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def tx2Hex(tx: Transaction): String = Hex.toHexString(Transaction.write(tx, protocolVersion))
- def hex2tx(hex: String) : Transaction = Transaction.read(hex, protocolVersion)
+ 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
@@ -48,11 +48,14 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
case JString(txid) => txid
}
- def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] =
+ def getRawTransaction(txId: String)(implicit ec: ExecutionContext): Future[String] =
client.invoke("getrawtransaction", txId) map {
- case JString(raw) => Transaction.read(raw)
+ case JString(raw) => raw
}
+ def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] =
+ getRawTransaction(txId).map(raw => Transaction.read(raw))
+
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double)
def fundTransaction(hex: String)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
@@ -95,7 +98,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
bestblockhash <- client.invoke("getbestblockhash").map(_.extract[String])
bestblock <- client.invoke("getblock", bestblockhash).map(b => (b \ "tx").extract[List[String]])
txs <- Future {
- for(txid <- mempool ++ bestblock) yield {
+ for (txid <- mempool ++ bestblock) yield {
Await.result(client.invoke("getrawtransaction", txid).map(json => {
Transaction.read(json.extract[String])
}).recover {
@@ -135,7 +138,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
witness = ScriptWitness(Seq(sig, pub))
tx2 = tx1.copy(witness = Seq(witness))
Some(pos1) = Scripts.findPublicKeyScriptIndex(tx2, anchorOutputScript)
- } yield(tx2, pos1)
+ } yield (tx2, pos1)
future
}
diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Channel.scala
index 041b5c553..38533137a 100644
--- a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Channel.scala
+++ b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Channel.scala
@@ -430,7 +430,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
case Success(signedTx) =>
blockchain ! Publish(signedTx)
blockchain ! WatchConfirmed(self, signedTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
- goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, mutualClosePublished = Some(signedTx))
+ goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourSignature = Some(d.ourSignature), mutualClosePublished = Some(signedTx))
case Failure(cause) =>
log.error(cause, "cannot verify their close signature")
throw new RuntimeException("cannot verify their close signature", cause)
@@ -449,7 +449,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
val signedTx = addSigs(d.commitments.ourParams, d.commitments.theirParams, d.commitments.anchorOutput.amount.toLong, finalTx, ourCloseSig.sig, theirSig)
blockchain ! Publish(signedTx)
blockchain ! WatchConfirmed(self, signedTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
- goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, mutualClosePublished = Some(signedTx))
+ goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourSignature = Some(ourCloseSig), mutualClosePublished = Some(signedTx))
} else {
stay using d.copy(ourSignature = ourCloseSig)
}
diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Register.scala
index 793c6727d..3b1f6daa1 100644
--- a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Register.scala
+++ b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Register.scala
@@ -25,7 +25,7 @@ import fr.acinq.eclair.{Boot, Globals}
* ├── client (0..m, transient)
* └── api
*/
-class Register extends Actor with ActorLogging {
+class Register(blockchain: ActorRef) extends Actor with ActorLogging {
import Register._
@@ -36,7 +36,7 @@ class Register extends Actor with ActorLogging {
val commit_priv = DeterministicWallet.derivePrivateKey(Globals.Node.extendedPrivateKey, 0L :: counter :: Nil)
val final_priv = DeterministicWallet.derivePrivateKey(Globals.Node.extendedPrivateKey, 1L :: counter :: Nil)
val params = OurChannelParams(Globals.default_locktime, commit_priv.secretkey :+ 1.toByte, final_priv.secretkey :+ 1.toByte, Globals.default_mindepth, Globals.commit_fee, "sha-seed".getBytes(), amount)
- context.actorOf(Props(classOf[AuthHandler], connection, Boot.blockchain, params), name = s"handler-${counter}")
+ context.actorOf(Props(classOf[AuthHandler], connection, blockchain, params), name = s"handler-${counter}")
context.become(main(counter + 1))
case ListChannels => sender ! context.children
}
@@ -44,6 +44,8 @@ class Register extends Actor with ActorLogging {
object Register {
+ def props(blockchain: ActorRef = Boot.blockchain) = Props(classOf[Register], blockchain)
+
// @formatter:off
case class CreateChannel(connection: ActorRef, anchorAmount: Option[Long])
case class ListChannels()
diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/io/Server.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/io/Server.scala
index 0878ec29a..444f63fd6 100644
--- a/eclair-demo/src/main/scala/fr/acinq/eclair/io/Server.scala
+++ b/eclair-demo/src/main/scala/fr/acinq/eclair/io/Server.scala
@@ -2,16 +2,16 @@ package fr.acinq.eclair.io
import java.net.InetSocketAddress
-import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
+import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props}
import akka.io.{IO, Tcp}
import com.typesafe.config.ConfigFactory
import fr.acinq.eclair.Boot
import fr.acinq.eclair.channel.Register.CreateChannel
/**
- * Created by PM on 27/10/2015.
- */
-class Server(address: InetSocketAddress) extends Actor with ActorLogging {
+ * Created by PM on 27/10/2015.
+ */
+class Server(address: InetSocketAddress, register: ActorRef) extends Actor with ActorLogging {
import Tcp._
import context.system
@@ -27,16 +27,19 @@ class Server(address: InetSocketAddress) extends Actor with ActorLogging {
case c@Connected(remote, local) =>
log.info(s"connected to $remote")
val connection = sender()
- Boot.register ! CreateChannel(connection, None)
+ register ! CreateChannel(connection, None)
}
}
object Server extends App {
- implicit val system = ActorSystem("system")
- val config = ConfigFactory.load()
- val server = system.actorOf(Server.props(config.getString("eclair.server.address"), config.getInt("eclair.server.port")), "server")
+ // implicit val system = ActorSystem("system")
+ // val config = ConfigFactory.load()
+ // val server = system.actorOf(Server.props(config.getString("eclair.server.address"), config.getInt("eclair.server.port")), "server")
- def props(address: InetSocketAddress): Props = Props(classOf[Server], address)
- def props(address: String, port: Int): Props = props(new InetSocketAddress(address, port))
+ def props(address: InetSocketAddress, register: ActorRef): Props = Props(classOf[Server], address, register)
+
+ def props(address: String, port: Int, register: ActorRef): Props = props(new InetSocketAddress(address, port), register)
+
+ def props(address: String, port: Int): Props = props(new InetSocketAddress(address, port), Boot.register)
}
diff --git a/eclair-demo/src/test/resources/application.conf b/eclair-demo/src/test/resources/application.conf
index b3cbecc61..74b0d6a0b 100644
--- a/eclair-demo/src/test/resources/application.conf
+++ b/eclair-demo/src/test/resources/application.conf
@@ -20,6 +20,9 @@ eclair {
closing-fee = 10000
routing-file = routes.txt
}
+lightning-cli {
+ path = "lightning-cli"
+}
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
diff --git a/eclair-demo/src/test/resources/logback-test.xml b/eclair-demo/src/test/resources/logback-test.xml
index 96c0d382e..74a0158d7 100644
--- a/eclair-demo/src/test/resources/logback-test.xml
+++ b/eclair-demo/src/test/resources/logback-test.xml
@@ -8,6 +8,8 @@
+
+
diff --git a/eclair-demo/src/test/scala/fr/acinq/eclair/channel/InteroperabilitySpec.scala b/eclair-demo/src/test/scala/fr/acinq/eclair/channel/InteroperabilitySpec.scala
new file mode 100644
index 000000000..de0921d12
--- /dev/null
+++ b/eclair-demo/src/test/scala/fr/acinq/eclair/channel/InteroperabilitySpec.scala
@@ -0,0 +1,189 @@
+package fr.acinq.eclair.channel
+
+import akka.actor.{ActorPath, ActorRef, ActorSystem, Props}
+import akka.pattern.ask
+import akka.testkit.TestKit
+import akka.util.Timeout
+import com.typesafe.config.ConfigFactory
+import fr.acinq.bitcoin.{BinaryData, Crypto}
+import fr.acinq.eclair._
+import fr.acinq.eclair.Globals._
+import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, PollingWatcher}
+import fr.acinq.eclair.channel.Register.ListChannels
+import fr.acinq.eclair.io.Server
+import org.json4s.JsonAST.JString
+import org.json4s.jackson.JsonMethods._
+import org.scalatest.FunSuiteLike
+
+import scala.concurrent.{Await, Future}
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.sys.process._
+
+/**
+ * this test is ignored by default
+ * to run it:
+ * mvn exec:java -Dexec.mainClass="org.scalatest.tools.Runner" \
+ * -Dexec.classpathScope="test" -Dexec.args="-s fr.acinq.eclair.channel.InteroperabilitySpec" \
+ * -Dlightning-cli.path=/home/fabrice/code/lightning-c.fdrn/daemon/lightning-cli
+ *
+ * You don't have to specify where lightning-cli is if it is on the PATH
+ */
+class InteroperabilitySpec extends TestKit(ActorSystem("test")) with FunSuiteLike {
+
+ import InteroperabilitySpec._
+
+ val config = ConfigFactory.load()
+ implicit val formats = org.json4s.DefaultFormats
+
+ val chain = Await.result(bitcoin_client.invoke("getblockchaininfo").map(json => (json \ "chain").extract[String]), 10 seconds)
+ assert(chain == "testnet" || chain == "regtest" || chain == "segnet4", "you should be on testnet or regtest or segnet4")
+
+ val blockchain = system.actorOf(Props(new PollingWatcher(new ExtendedBitcoinClient(bitcoin_client))), name = "blockchain")
+ val register = system.actorOf(Register.props(blockchain), name = "register")
+ val server = system.actorOf(Server.props(config.getString("eclair.server.address"), config.getInt("eclair.server.port"), register), "server")
+
+ val lncli = new LightingCli(config.getString("lightning-cli.path"))
+ val btccli = new ExtendedBitcoinClient(Globals.bitcoin_client)
+ implicit val timeout = Timeout(30 seconds)
+
+ def actorPathToChannelId(channelId: BinaryData): ActorPath =
+ system / "register" / "handler-*" / "channel" / s"*-${channelId}"
+
+ def sendCommand(channel_id: String, cmd: Command): Future[String] = {
+ system.actorSelection(actorPathToChannelId(channel_id)).resolveOne().map(actor => {
+ actor ! cmd
+ "ok"
+ })
+ }
+
+ def connect(host: String, port: Int): Future[Unit] = {
+ val address = lncli.fund
+ val future = for {
+ txid <- btccli.sendFromAccount("", address, 0.03)
+ tx <- btccli.getRawTransaction(txid)
+ } yield lncli.connect(host, port, tx)
+ future
+ }
+
+ def listChannels: Future[Iterable[RES_GETINFO]] = {
+ implicit val timeout = Timeout(5 seconds)
+ (register ? ListChannels).mapTo[Iterable[ActorRef]]
+ .flatMap(l => Future.sequence(l.map(c => (c ? CMD_GETINFO).mapTo[RES_GETINFO])))
+ }
+
+ def waitForState(state: State): Future[Unit] = {
+ listChannels.map(_.map(_.state)).flatMap(current =>
+ if (current.toSeq == Seq(state))
+ Future.successful(())
+ else {
+ Thread.sleep(5000)
+ waitForState(state)
+ }
+ )
+ }
+
+ val R = BinaryData("0102030405060708010203040506070801020304050607080102030405060708")
+ val H: BinaryData = Crypto.sha256(R)
+
+ test("connect to lighningd") {
+ val future = for {
+ _ <- connect("localhost", 45000)
+ _ <- waitForState(OPEN_WAITING_THEIRANCHOR)
+ } yield ()
+ Await.result(future, 30 seconds)
+ }
+
+ test("reach normal state") {
+ val future = for {
+ _ <- btccli.client.invoke("generate", 10)
+ _ <- waitForState(NORMAL)
+ } yield ()
+ Await.result(future, 45 seconds)
+ }
+
+ test("fulfill an HTLC") {
+ val future = for {
+ channelId <- listChannels.map(_.head).map(_.channelid.toString)
+ peer = lncli.getPeers.head
+ _ = lncli.newhtlc(peer.peerid, 70000000, (System.currentTimeMillis() / 1000) + 100000, H)
+ _ = Thread.sleep(500)
+ _ <- sendCommand(channelId, CMD_SIGN)
+ _ = Thread.sleep(500)
+ htlcid <- listChannels.map(_.head).map(_.data.asInstanceOf[DATA_NORMAL].commitments.theirCommit.spec.htlcs.head.id)
+ _ <- sendCommand(channelId, CMD_FULFILL_HTLC(htlcid, R))
+ _ <- sendCommand(channelId, CMD_SIGN)
+ peer1 = lncli.getPeers.head
+ _ = assert(peer1.their_amount + peer1.their_fee == 70000000)
+ } yield ()
+ Await.result(future, 30 seconds)
+ }
+
+ test("close the channel") {
+ val peer = lncli.getPeers.head
+ lncli.close(peer.peerid)
+
+ val future = for {
+ _ <- waitForState(CLOSING)
+ _ <- btccli.client.invoke("generate", 10)
+ _ <- waitForState(CLOSED)
+ } yield ()
+
+ Await.result(future, 45 seconds)
+ }
+}
+
+object InteroperabilitySpec {
+
+ object LightningCli {
+
+ case class Peers(peers: Seq[Peer])
+
+ case class Peer(name: String, state: String, peerid: String, our_amount: Long, our_fee: Long, their_amount: Long, their_fee: Long)
+
+ }
+
+ class LightingCli(path: String) {
+
+ import LightningCli._
+
+ implicit val formats = org.json4s.DefaultFormats
+
+ /**
+ *
+ * @return a funding address that can be used to connect to another node
+ */
+ def fund: String = {
+ val raw = s"$path newaddr" !!
+ val json = parse(raw)
+ val JString(address) = json \ "address"
+ address
+ }
+
+ /**
+ * connect to another node
+ *
+ * @param host node address
+ * @param port node port
+ * @param tx transaction that sends money to a funding address generated with the "fund" method
+ */
+ def connect(host: String, port: Int, tx: String): Unit = {
+ assert(s"$path connect $host $port $tx".! == 0)
+ }
+
+ def close(peerId: String): Unit = {
+ assert(s"$path close $peerId".! == 0)
+ }
+
+ def getPeers: Seq[Peer] = {
+ val raw = s"$path getpeers" !!
+
+ parse(raw).extract[Peers].peers
+ }
+
+ def newhtlc(peerid: String, amount: Long, expiry: Long, rhash: BinaryData): Unit = {
+ assert(s"$path newhtlc $peerid $amount $expiry $rhash".! == 0)
+ }
+ }
+
+}