1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 22:46:44 +01:00

add interoperability test with lightningd

This commit is contained in:
sstone 2016-06-09 16:21:14 +02:00
parent 2ba08d6ad4
commit ca5cb7e8bb
8 changed files with 222 additions and 20 deletions

View file

@ -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 {

View file

@ -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
}

View file

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

View file

@ -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()

View file

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

View file

@ -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"

View file

@ -8,6 +8,8 @@
</encoder>
</appender>
<logger name="com.ning.http" level="INFO"/>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>

View file

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