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:
parent
2ba08d6ad4
commit
ca5cb7e8bb
8 changed files with 222 additions and 20 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.ning.http" level="INFO"/>
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue