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) + } + } + +}