Compare commits
105 Commits
applicatio
...
wip-flare
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a85afb5b9 | ||
|
|
d2d2d673cb | ||
|
|
1cb24d3082 | ||
|
|
e21806e862 | ||
|
|
deea68fac1 | ||
|
|
f7a5852e0f | ||
|
|
c9bf29a957 | ||
|
|
fd7c109eb1 | ||
|
|
a96fdec7f3 | ||
|
|
ca7922971c | ||
|
|
9bba401543 | ||
|
|
457841120a | ||
|
|
b5ae68703f | ||
|
|
e40290875f | ||
|
|
7e055564df | ||
|
|
6ca4957f3e | ||
|
|
1647054ac2 | ||
|
|
96576ca662 | ||
|
|
8ca1fd5441 | ||
|
|
96572c884c | ||
|
|
ad0b3beed3 | ||
|
|
df0f0df152 | ||
|
|
5723cee340 | ||
|
|
b7b84f301f | ||
|
|
a20b7ba456 | ||
|
|
b843ea4ae3 | ||
|
|
cd1ea4f1a4 | ||
|
|
1fffdfd873 | ||
|
|
87568fbb55 | ||
|
|
d254c035f5 | ||
|
|
84b8279987 | ||
|
|
120fbebae8 | ||
|
|
990d0fc8f4 | ||
|
|
f531127988 | ||
|
|
a4826918b0 | ||
|
|
08a2039ac5 | ||
|
|
e31fa6fb0c | ||
|
|
affcff3673 | ||
|
|
d5a6f02368 | ||
|
|
d6733975aa | ||
|
|
b6218da288 | ||
|
|
6be815afe5 | ||
|
|
5ea523a08e | ||
|
|
bade5ddf0d | ||
|
|
1a19f58771 | ||
|
|
a73c3f2b29 | ||
|
|
72946faa62 | ||
|
|
9b97bb664c | ||
|
|
2fc52ffbd8 | ||
|
|
78d2f8d961 | ||
|
|
f234b1fea2 | ||
|
|
cde9181676 | ||
|
|
1adf972caf | ||
|
|
1b0c7ecee3 | ||
|
|
1cd5119ae0 | ||
|
|
47253233c6 | ||
|
|
0544b804e9 | ||
|
|
d2b40c61bd | ||
|
|
d82a4d7940 | ||
|
|
71eba309af | ||
|
|
7c857c9be5 | ||
|
|
a34538de3a | ||
|
|
ac51e64247 | ||
|
|
d24956bdac | ||
|
|
d82062898f | ||
|
|
92ae29fdef | ||
|
|
35da8d7dad | ||
|
|
bb07d794a1 | ||
|
|
4bea3741ed | ||
|
|
fbdfed4ec7 | ||
|
|
2bd0c52f38 | ||
|
|
10aaedb870 | ||
|
|
98fdc60823 | ||
|
|
5ce8d8494e | ||
|
|
493d826881 | ||
|
|
4f92e34c26 | ||
|
|
51a6a84503 | ||
|
|
ac5a979517 | ||
|
|
c0186bcd94 | ||
|
|
8a583cd813 | ||
|
|
b0dbe47d49 | ||
|
|
779a366d7c | ||
|
|
94cf8fcff9 | ||
|
|
2232239f9e | ||
|
|
e601912df6 | ||
|
|
d15ccec680 | ||
|
|
5ec9ea2c23 | ||
|
|
2689110e30 | ||
|
|
81e8211a45 | ||
|
|
c99a2eb7cb | ||
|
|
ee08128fa8 | ||
|
|
02830850a1 | ||
|
|
23dc2a32ef | ||
|
|
d2c340030f | ||
|
|
0d642bf52f | ||
|
|
bc9d727fd2 | ||
|
|
9e96fb94fa | ||
|
|
d715e7eaa8 | ||
|
|
d91fc66b8c | ||
|
|
419c747bb9 | ||
|
|
8b4d38756c | ||
|
|
76406145fa | ||
|
|
5245cbc006 | ||
|
|
7f7b9e5711 | ||
|
|
e847ed5f4e |
2
.gitignore
vendored
2
.gitignore
vendored
@ -27,3 +27,5 @@ DeleteMe*.*
|
||||
*~
|
||||
|
||||
result.txt
|
||||
*.gv
|
||||
*.dot
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>fr.acinq.eclair</groupId>
|
||||
<artifactId>eclair_2.11</artifactId>
|
||||
<version>0.2-SNAPSHOT</version>
|
||||
<version>0.2-flare-dynamic-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>eclair-node_2.11</artifactId>
|
||||
@ -42,6 +42,17 @@
|
||||
<customDescriptor>-${git.commit.id.abbrev}-capsule-fat</customDescriptor>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>jar-simul</id>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<appClass>fr.acinq.protos.flare.Simulator</appClass>
|
||||
<type>fat</type>
|
||||
<customDescriptor>-${git.commit.id.abbrev}-simul-capsule-fat</customDescriptor>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
@ -99,21 +110,6 @@
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.kitteh.irc</groupId>
|
||||
<artifactId>client-lib</artifactId>
|
||||
<version>2.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
<artifactId>jgrapht-core</artifactId>
|
||||
<version>0.9.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
<artifactId>jgrapht-ext</artifactId>
|
||||
<version>0.9.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- This is to get rid of '[WARNING] warning: Class javax.annotation.Nonnull not found - continuing with a stub.' compile errors git s-->
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
@ -126,6 +122,11 @@
|
||||
<artifactId>janino</artifactId>
|
||||
<version>2.5.10</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.papertrailapp</groupId>
|
||||
<artifactId>logback-syslog4j</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-testkit_${scala.version.short}</artifactId>
|
||||
@ -137,5 +138,22 @@
|
||||
<version>18.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- simulator -->
|
||||
<dependency>
|
||||
<groupId>org.graphstream</groupId>
|
||||
<artifactId>gs-core</artifactId>
|
||||
<version>1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.graphstream</groupId>
|
||||
<artifactId>gs-algo</artifactId>
|
||||
<version>1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
<!-- /simulator -->
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -23,6 +23,11 @@ eclair {
|
||||
base-fee = 546000
|
||||
proportional-fee = 10
|
||||
payment-handler = "local"
|
||||
flare {
|
||||
radius = 2
|
||||
beacon-count = 1
|
||||
beacon-reactivate-count = 5
|
||||
}
|
||||
}
|
||||
akka {
|
||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||
|
||||
@ -8,9 +8,10 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="fr.acinq.eclair.channel" level="DEBUG" />
|
||||
<logger name="fr.acinq.eclair.channel.Register" level="DEBUG" />
|
||||
<logger name="fr.acinq.protos" level="DEBUG" />
|
||||
<logger name="fr.acinq.eclair.channel" level="DEBUG"/>
|
||||
<logger name="fr.acinq.eclair.channel.Register" level="DEBUG"/>
|
||||
<logger name="fr.acinq.eclair.router.FlareRouter" level="INFO"/>
|
||||
<logger name="fr.acinq.protos" level="DEBUG"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
|
||||
@ -4,21 +4,22 @@ import javafx.application.Application
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.util.Timeout
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.util.Timeout
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import fr.acinq.bitcoin.{BinaryData, BitcoinJsonRPCClient, OutPoint, Satoshi, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.api.Service
|
||||
import fr.acinq.eclair.blockchain.peer.{NewBlock, NewTransaction}
|
||||
import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, PeerWatcher}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.LightningCrypto
|
||||
import fr.acinq.eclair.gui.MainWindow
|
||||
import fr.acinq.eclair.io.{Client, Server}
|
||||
import fr.acinq.eclair.router._
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.concurrent.{Await, ExecutionContext}
|
||||
import scala.concurrent.duration._
|
||||
import fr.acinq.bitcoin.{BitcoinJsonRPCClient, Satoshi}
|
||||
import fr.acinq.eclair.blockchain.peer.PeerClient
|
||||
import fr.acinq.eclair.gui.MainWindow
|
||||
import fr.acinq.eclair.router.{ChannelSelector, IRCWatcher, PaymentSpawner, Router}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
@ -30,39 +31,71 @@ object Boot extends App with Logging {
|
||||
}
|
||||
}
|
||||
|
||||
class FakeBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinClient(new BitcoinJsonRPCClient("", "", "", 0)) {
|
||||
|
||||
client.client.close()
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
system.scheduler.schedule(3 seconds, 3 seconds, new Runnable {
|
||||
override def run(): Unit = system.eventStream.publish(NewBlock(null)) // blocks are not actually interpreted
|
||||
})
|
||||
|
||||
override def makeAnchorTx(ourCommitPub: BinaryData, theirCommitPub: BinaryData, amount: Satoshi)(implicit ec: ExecutionContext): Future[(Transaction, Int)] = {
|
||||
val fakeTxid = LightningCrypto.randomKeyPair().priv
|
||||
val txIn = TxIn(OutPoint(fakeTxid, 0), BinaryData("00"), 0)
|
||||
val anchorTx = Transaction(version = 1,
|
||||
txIn = txIn :: Nil,
|
||||
txOut = TxOut(amount, Scripts.anchorPubkeyScript(ourCommitPub, theirCommitPub)) :: Nil,
|
||||
lockTime = 0
|
||||
)
|
||||
Future.successful((anchorTx, 0))
|
||||
}
|
||||
|
||||
override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = {
|
||||
system.eventStream.publish(NewTransaction(tx))
|
||||
Future.successful(tx.txid.toString())
|
||||
}
|
||||
|
||||
override def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] = Future.successful(Some(10))
|
||||
|
||||
override def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] = ???
|
||||
|
||||
override def fundTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = ???
|
||||
|
||||
override def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = ???
|
||||
|
||||
}
|
||||
|
||||
class Setup extends Logging {
|
||||
|
||||
logger.info(s"hello!")
|
||||
logger.info(s"nodeid=${Globals.Node.publicKey}")
|
||||
val config = ConfigFactory.load()
|
||||
|
||||
val bitcoin_client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
|
||||
user = config.getString("eclair.bitcoind.rpcuser"),
|
||||
password = config.getString("eclair.bitcoind.rpcpassword"),
|
||||
host = config.getString("eclair.bitcoind.host"),
|
||||
port = config.getInt("eclair.bitcoind.rpcport")))
|
||||
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
implicit val ec = ExecutionContext.Implicits.global
|
||||
val (chain, blockCount) = Await.result(bitcoin_client.client.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long])), 10 seconds)
|
||||
assert(chain == "testnet" || chain == "regtest" || chain == "segnet4", "you should be on testnet or regtest or segnet4")
|
||||
val bitcoinVersion = Await.result(bitcoin_client.client.invoke("getinfo").map(json => (json \ "version").extract[String]), 10 seconds)
|
||||
|
||||
implicit lazy val system = ActorSystem()
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
|
||||
val peer = system.actorOf(Props[PeerClient], "bitcoin-peer")
|
||||
val watcher = system.actorOf(PeerWatcher.props(bitcoin_client, blockCount), name = "watcher")
|
||||
val bitcoin_client = new FakeBitcoinClient()
|
||||
val chain = "fake"
|
||||
val bitcoinVersion = "fake"
|
||||
|
||||
val watcher = system.actorOf(PeerWatcher.props(bitcoin_client, 1000), name = "watcher")
|
||||
val paymentHandler = config.getString("eclair.payment-handler") match {
|
||||
case "local" => system.actorOf(Props[LocalPaymentHandler], name = "payment-handler")
|
||||
case "noop" => system.actorOf(Props[NoopPaymentHandler], name = "payment-handler")
|
||||
}
|
||||
val register = system.actorOf(Register.props(watcher, paymentHandler), name = "register")
|
||||
val radius = config.getInt("eclair.flare.radius")
|
||||
val beaconCount = config.getInt("eclair.flare.beacon-count")
|
||||
val beaconReactivateCount = config.getInt("eclair.flare.beacon-reactivate-count")
|
||||
logger.info(s"flare params radius=$radius beaconCount=$beaconCount beaconReactivateCount=$beaconReactivateCount")
|
||||
val router = system.actorOf(Props(new FlareRouter(Globals.Node.publicKey, radius, beaconCount, beaconReactivateCount = beaconReactivateCount)), name = "router")
|
||||
val register = system.actorOf(Register.props(watcher, paymentHandler, router), name = "register")
|
||||
val selector = system.actorOf(Props[ChannelSelector], name = "selector")
|
||||
val router = system.actorOf(Props[Router], name = "router")
|
||||
val ircWatcher = system.actorOf(Props[IRCWatcher], "irc")
|
||||
val paymentSpawner = system.actorOf(PaymentSpawner.props(router, selector, blockCount), "payment-spawner")
|
||||
val paymentSpawner = system.actorOf(PaymentSpawner.props(router, selector, 1000), "payment-spawner")
|
||||
val server = system.actorOf(Server.props(config.getString("eclair.server.host"), config.getInt("eclair.server.port"), register), "server")
|
||||
|
||||
val _setup = this
|
||||
|
||||
@ -2,8 +2,9 @@ package fr.acinq.eclair
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import fr.acinq.bitcoin.{BinaryData, DeterministicWallet}
|
||||
import fr.acinq.eclair.crypto.LightningCrypto
|
||||
import lightning.locktime
|
||||
import lightning.locktime.Locktime.{Blocks}
|
||||
import lightning.locktime.Locktime.Blocks
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
@ -15,7 +16,7 @@ object Globals {
|
||||
val config = ConfigFactory.load()
|
||||
|
||||
object Node {
|
||||
val seed: BinaryData = config.getString("eclair.node.seed")
|
||||
val seed: BinaryData = LightningCrypto.randomKeyPair().priv
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil)
|
||||
val privateKey = extendedPrivateKey.secretkey
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package fr.acinq.eclair.api
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.State
|
||||
@ -42,3 +44,11 @@ class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
|
||||
case x: ShaChain => JNull
|
||||
}
|
||||
))
|
||||
|
||||
class BigIntegerSerializer extends CustomSerializer[BigInteger](format => ( {
|
||||
case JString(x) => // NOT IMPLEMENTED
|
||||
new BigInteger("00")
|
||||
}, {
|
||||
case x: BigInteger => JString(BinaryData(x.toByteArray).toString())
|
||||
}
|
||||
))
|
||||
|
||||
@ -1,26 +1,28 @@
|
||||
package fr.acinq.eclair.api
|
||||
|
||||
import java.util.Base64
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.http.scaladsl.model.headers.RawHeader
|
||||
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCodes}
|
||||
import akka.util.Timeout
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel.Register.{ListChannels, SendCommand}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.router.FlareRouter.{FlareInfo, RouteRequest, RouteResponse}
|
||||
import grizzled.slf4j.Logging
|
||||
import lightning.{channel_open, channel_state_update, payment_request}
|
||||
import org.json4s.JsonAST.JString
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.JsonMethods._
|
||||
import org.json4s.jackson.Serialization
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
import akka.pattern.ask
|
||||
import fr.acinq.eclair.channel.Register.{ListChannels, SendCommand}
|
||||
import fr.acinq.eclair.router.{ChannelDesc, CreatePayment}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
@ -39,7 +41,7 @@ trait Service extends Logging {
|
||||
|
||||
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
|
||||
|
||||
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new Sha256Serializer + new ShaChainSerializer
|
||||
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new Sha256Serializer + new ShaChainSerializer + new BigIntegerSerializer
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
|
||||
def connect(host: String, port: Int, amount: Satoshi): Unit
|
||||
@ -74,11 +76,31 @@ trait Service extends Logging {
|
||||
(register ? ListChannels).mapTo[Iterable[ActorRef]]
|
||||
.flatMap(l => Future.sequence(l.map(c => c ? CMD_GETINFO)))
|
||||
case JsonRPCBody(_, _, "network", _) =>
|
||||
(router ? 'network).mapTo[Iterable[ChannelDesc]]
|
||||
(router ? 'network).mapTo[Iterable[channel_open]]
|
||||
case JsonRPCBody(_, _, "beacons", _) =>
|
||||
(router ? 'beacons).mapTo[Iterable[BinaryData]]
|
||||
case JsonRPCBody(_, _, "dot", _) =>
|
||||
(router ? 'dot).mapTo[String]
|
||||
case JsonRPCBody(_, _, "addhtlc", JInt(amount) :: JString(rhash) :: JString(nodeId) :: Nil) =>
|
||||
(paymentSpawner ? CreatePayment(amount.toInt, BinaryData(rhash), BinaryData(nodeId))).mapTo[ChannelEvent]
|
||||
(paymentSpawner ? payment_request(BinaryData(nodeId), amount.toInt, BinaryData(rhash), null, null)).mapTo[ChannelEvent]
|
||||
case JsonRPCBody(_, _, "pay", JString(base64) :: Nil) =>
|
||||
val paymentRequest = lightning.payment_request.parseFrom(Base64.getDecoder().decode(base64))
|
||||
(paymentSpawner ? paymentRequest).mapTo[ChannelEvent]
|
||||
case JsonRPCBody(_, _, "genh", _) =>
|
||||
(paymentHandler ? 'genh).mapTo[BinaryData]
|
||||
case JsonRPCBody(_, _, "genpaymentrequest", JInt(amount) :: Nil) =>
|
||||
val future1 = (paymentHandler ? 'genh).mapTo[BinaryData]
|
||||
val future2 = (router ? 'network).mapTo[Seq[channel_open]]
|
||||
val future3 = (router ? 'states).mapTo[Seq[channel_state_update]]
|
||||
val future4 = for {
|
||||
h <- future1
|
||||
channels <- future2
|
||||
states <- future3
|
||||
} yield lightning.payment_request(Globals.Node.publicKey, amount.toInt, h, lightning.routing_table(channels), states)
|
||||
future4.map(r => Base64.getEncoder.encodeToString(r.toByteArray))
|
||||
case JsonRPCBody(_, _, "findroute", JString(base64) :: Nil) =>
|
||||
val paymentRequest = lightning.payment_request.parseFrom(Base64.getDecoder.decode(base64))
|
||||
(router ? RouteRequest(paymentRequest)).mapTo[RouteResponse]
|
||||
case JsonRPCBody(_, _, "sign", JString(channel) :: Nil) =>
|
||||
(register ? SendCommand(channel, CMD_SIGN)).mapTo[ActorRef].map(_ => "ok")
|
||||
case JsonRPCBody(_, _, "fulfillhtlc", JString(channel) :: JDouble(id) :: JString(r) :: Nil) =>
|
||||
@ -97,6 +119,12 @@ trait Service extends Logging {
|
||||
"fulfillhtlc (channel_id, htlc_id, r): fulfill an htlc",
|
||||
"close (channel_id): close a channel",
|
||||
"help: display this message"))
|
||||
case JsonRPCBody(_, _, "suicide", _) =>
|
||||
logger.warn("commiting suicide")
|
||||
System.exit(0)
|
||||
Future.successful("")
|
||||
case JsonRPCBody(_, _, "flare_info", _) =>
|
||||
(router ? 'info).mapTo[FlareInfo]
|
||||
case _ => Future.failed(new RuntimeException("method not found"))
|
||||
}
|
||||
|
||||
|
||||
@ -30,11 +30,8 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
|
||||
case t: JsonRPCError if t.code == -5 => None
|
||||
}
|
||||
|
||||
def isUnspent(txId: String, outputIndex: Int)(implicit ec: ExecutionContext): Future[Boolean] =
|
||||
client.invoke("gettxout", txId, outputIndex, true) // mempool=true so that we are warned as soon as possible
|
||||
.map(json => json != JNull)
|
||||
|
||||
/**
|
||||
* *used in interop test*
|
||||
* tell bitcoind to sent bitcoins from a specific local account
|
||||
*
|
||||
* @param account name of the local account to send bitcoins from
|
||||
@ -48,6 +45,11 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
|
||||
case JString(txid) => txid
|
||||
}
|
||||
|
||||
/**
|
||||
* @param txId
|
||||
* @param ec
|
||||
* @return
|
||||
*/
|
||||
def getRawTransaction(txId: String)(implicit ec: ExecutionContext): Future[String] =
|
||||
client.invoke("getrawtransaction", txId) collect {
|
||||
case JString(raw) => raw
|
||||
@ -90,26 +92,6 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
|
||||
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
|
||||
publishTransaction(tx2Hex(tx))
|
||||
|
||||
// TODO : this is very dirty
|
||||
// we only check the memory pool and the last block, and throw an error if tx was not found
|
||||
def findSpendingTransaction(txid: String, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = {
|
||||
for {
|
||||
mempool <- client.invoke("getrawmempool").map(_.extract[List[String]])
|
||||
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 {
|
||||
Await.result(client.invoke("getrawtransaction", txid).map(json => {
|
||||
Transaction.read(json.extract[String])
|
||||
}).recover {
|
||||
case t: Throwable => Transaction(0, Seq(), Seq(), 0)
|
||||
}, 20 seconds)
|
||||
}
|
||||
}
|
||||
tx = txs.find(tx => tx.txIn.exists(input => input.outPoint.txid == txid && input.outPoint.index == outputIndex)).getOrElse(throw new RuntimeException("tx not found!"))
|
||||
} yield tx
|
||||
}
|
||||
|
||||
def makeAnchorTx(ourCommitPub: BinaryData, theirCommitPub: BinaryData, amount: Satoshi)(implicit ec: ExecutionContext): Future[(Transaction, Int)] = {
|
||||
val anchorOutputScript = channel.Scripts.anchorPubkeyScript(ourCommitPub, theirCommitPub)
|
||||
val tx = Transaction(version = 2, txIn = Seq.empty[TxIn], txOut = TxOut(amount, anchorOutputScript) :: Nil, lockTime = 0)
|
||||
|
||||
@ -27,8 +27,7 @@ class PeerWatcher(client: ExtendedBitcoinClient, blockCount: Long)(implicit ec:
|
||||
watches.collect {
|
||||
case w@WatchSpent(channel, txid, outputIndex, minDepth, event)
|
||||
if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
channel ! (BITCOIN_ANCHOR_SPENT, tx)
|
||||
self ! ('remove, w)
|
||||
self ! ('fire, w, tx)
|
||||
case _ => {}
|
||||
}
|
||||
|
||||
@ -40,8 +39,7 @@ class PeerWatcher(client: ExtendedBitcoinClient, blockCount: Long)(implicit ec:
|
||||
client.getTxConfirmations(txId.toString).map(_ match {
|
||||
case Some(confirmations) if confirmations >= minDepth =>
|
||||
log.info(s"triggering $w")
|
||||
channel ! event
|
||||
self ! ('remove, w)
|
||||
self ! ('fire, w)
|
||||
case _ => {} // not enough confirmations
|
||||
})
|
||||
}
|
||||
@ -59,7 +57,12 @@ class PeerWatcher(client: ExtendedBitcoinClient, blockCount: Long)(implicit ec:
|
||||
context.watch(w.channel)
|
||||
context.become(watching(watches + w, block2tx, currentBlockCount))
|
||||
|
||||
case ('remove, w: Watch) if watches.contains(w) =>
|
||||
case ('fire, w: WatchSpent, tx: Transaction) if watches.contains(w) =>
|
||||
w.channel ! (BITCOIN_ANCHOR_SPENT, tx)
|
||||
context.become(watching(watches - w, block2tx, currentBlockCount))
|
||||
|
||||
case ('fire, w: WatchConfirmed) if watches.contains(w) =>
|
||||
w.channel ! w.event
|
||||
context.become(watching(watches - w, block2tx, currentBlockCount))
|
||||
|
||||
case Publish(tx) =>
|
||||
@ -87,6 +90,6 @@ class PeerWatcher(client: ExtendedBitcoinClient, blockCount: Long)(implicit ec:
|
||||
|
||||
object PeerWatcher {
|
||||
|
||||
def props(client: ExtendedBitcoinClient, initialBlockCount: Long)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(classOf[PeerWatcher], client, initialBlockCount, ec)
|
||||
def props(client: ExtendedBitcoinClient, initialBlockCount: Long)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new PeerWatcher(client, initialBlockCount))
|
||||
|
||||
}
|
||||
@ -636,7 +636,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
|
||||
}
|
||||
|
||||
onTransition {
|
||||
case previousState -> currentState => context.system.eventStream.publish(ChannelChangedState(self, theirNodeId, previousState, currentState, stateData))
|
||||
case previousState -> currentState => context.system.eventStream.publish(ChannelChangedState(self, context.parent, theirNodeId, previousState, currentState, stateData))
|
||||
}
|
||||
|
||||
/*
|
||||
@ -667,7 +667,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
|
||||
upstream ! CMD_SIGN
|
||||
case Failure(t: Throwable) =>
|
||||
// TODO : send "fail route error"
|
||||
log.warning(s"couldn't resolve upstream node, htlc #${add.id} will timeout", t)
|
||||
log.warning(s"couldn't resolve upstream node, htlc #${add.id} will timeout " + t)
|
||||
}
|
||||
case route_step(amount, Next.End(true)) +: rest =>
|
||||
log.info(s"we are the final recipient of htlc #${add.id}")
|
||||
|
||||
@ -14,12 +14,15 @@ case class ChannelCreated(channel: ActorRef, params: OurChannelParams, theirNode
|
||||
|
||||
case class ChannelIdAssigned(channel: ActorRef, channelId: BinaryData, amount: Long) extends ChannelEvent
|
||||
|
||||
case class ChannelChangedState(channel: ActorRef, theirNodeId: BinaryData, previousState: State, currentState: State, currentData: Data) extends ChannelEvent
|
||||
case class ChannelChangedState(channel: ActorRef, authHandler: ActorRef, theirNodeId: BinaryData, previousState: State, currentState: State, currentData: Data) extends ChannelEvent
|
||||
|
||||
case class ChannelSignatureReceived(channel: ActorRef, Commitments: Commitments) extends ChannelEvent
|
||||
|
||||
case class PaymentSent(channel: ActorRef, h: sha256_hash) extends ChannelEvent
|
||||
|
||||
case class PaymentFailed(channel: ActorRef, h: sha256_hash, reason: String) extends ChannelEvent
|
||||
trait PaymentEvent
|
||||
|
||||
case class PaymentReceived(channel: ActorRef, h: sha256_hash) extends ChannelEvent
|
||||
case class PaymentSent(channel: ActorRef, h: sha256_hash) extends PaymentEvent
|
||||
|
||||
case class PaymentFailed(channel: ActorRef, h: sha256_hash, reason: String) extends PaymentEvent
|
||||
|
||||
case class PaymentReceived(channel: ActorRef, h: sha256_hash) extends PaymentEvent
|
||||
|
||||
@ -1,28 +1,27 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
import akka.actor.{Actor, ActorLogging}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto}
|
||||
import fr.acinq.eclair._
|
||||
import lightning.update_add_htlc
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 17/06/2016.
|
||||
*/
|
||||
class LocalPaymentHandler extends Actor with ActorLogging {
|
||||
|
||||
val random = SecureRandom.getInstanceStrong
|
||||
// see http://bugs.java.com/view_bug.do?bug_id=6521844
|
||||
//val random = SecureRandom.getInstanceStrong
|
||||
val random = new Random()
|
||||
|
||||
def generateR(): BinaryData = {
|
||||
val r = Array.fill[Byte](32)(0)
|
||||
random.nextBytes(r)
|
||||
r
|
||||
}
|
||||
|
||||
context.become(run(Map()))
|
||||
|
||||
override def receive: Receive = ???
|
||||
override def receive: Receive = run(Map())
|
||||
|
||||
//TODO: store this map on file ?
|
||||
def run(h2r: Map[BinaryData, BinaryData]): Receive = {
|
||||
|
||||
@ -28,7 +28,7 @@ import scala.concurrent.duration._
|
||||
* ├── client (0..m, transient)
|
||||
* └── api
|
||||
*/
|
||||
class Register(blockchain: ActorRef, paymentHandler: ActorRef) extends Actor with ActorLogging {
|
||||
class Register(blockchain: ActorRef, paymentHandler: ActorRef, router: ActorRef) extends Actor with ActorLogging {
|
||||
|
||||
import Register._
|
||||
|
||||
@ -39,7 +39,7 @@ class Register(blockchain: ActorRef, paymentHandler: ActorRef) extends Actor wit
|
||||
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, Globals.Node.seed, amount, Some(Globals.autosign_interval))
|
||||
val channel = context.actorOf(AuthHandler.props(connection, blockchain, paymentHandler, params), name = s"auth-handler-${counter}")
|
||||
val channel = context.actorOf(AuthHandler.props(connection, blockchain, paymentHandler, router, params), name = s"auth-handler-${counter}")
|
||||
context.become(main(counter + 1))
|
||||
case ListChannels => sender ! context.children
|
||||
case SendCommand(channelId, cmd) =>
|
||||
@ -55,7 +55,7 @@ class Register(blockchain: ActorRef, paymentHandler: ActorRef) extends Actor wit
|
||||
|
||||
object Register {
|
||||
|
||||
def props(blockchain: ActorRef, paymentHandler: ActorRef) = Props(classOf[Register], blockchain, paymentHandler)
|
||||
def props(blockchain: ActorRef, paymentHandler: ActorRef, router: ActorRef) = Props(new Register(blockchain, paymentHandler, router))
|
||||
|
||||
// @formatter:off
|
||||
case class CreateChannel(connection: ActorRef, anchorAmount: Option[Satoshi])
|
||||
|
||||
@ -113,9 +113,9 @@ object Scripts {
|
||||
// @formatter:off
|
||||
OP_HASH160 :: OP_PUSHDATA(ripemd160(hashOfSecret)) :: OP_EQUAL ::
|
||||
OP_IF ::
|
||||
OP_PUSHDATA(keyIfSecretKnown) ::
|
||||
OP_PUSHDATA(keyIfSecretKnown) ::
|
||||
OP_ELSE ::
|
||||
OP_PUSHDATA(encodeNumber(reltimeout)) :: OP_CHECKSEQUENCEVERIFY :: OP_DROP :: OP_PUSHDATA(delayedKey) ::
|
||||
OP_PUSHDATA(encodeNumber(reltimeout)) :: OP_CHECKSEQUENCEVERIFY :: OP_DROP :: OP_PUSHDATA(delayedKey) ::
|
||||
OP_ENDIF ::
|
||||
OP_CHECKSIG :: Nil
|
||||
// @formatter:on
|
||||
@ -130,9 +130,9 @@ object Scripts {
|
||||
OP_PUSHDATA(ripemd160(rhash)) :: OP_EQUAL ::
|
||||
OP_SWAP :: OP_PUSHDATA(ripemd160(commit_revoke)) :: OP_EQUAL :: OP_ADD ::
|
||||
OP_IF ::
|
||||
OP_PUSHDATA(theirkey) ::
|
||||
OP_PUSHDATA(theirkey) ::
|
||||
OP_ELSE ::
|
||||
OP_PUSHDATA(encodeNumber(abstimeout)) :: OP_CHECKLOCKTIMEVERIFY :: OP_PUSHDATA(encodeNumber(reltimeout)) :: OP_CHECKSEQUENCEVERIFY :: OP_2DROP :: OP_PUSHDATA(ourkey) ::
|
||||
OP_PUSHDATA(encodeNumber(abstimeout)) :: OP_CHECKLOCKTIMEVERIFY :: OP_PUSHDATA(encodeNumber(reltimeout)) :: OP_CHECKSEQUENCEVERIFY :: OP_2DROP :: OP_PUSHDATA(ourkey) ::
|
||||
OP_ENDIF ::
|
||||
OP_CHECKSIG :: Nil
|
||||
// @formatter:on
|
||||
@ -143,18 +143,18 @@ object Scripts {
|
||||
assert(abstimeout > 16, s"abstimeout=$abstimeout must be greater than 16")
|
||||
// @formatter:off
|
||||
OP_SIZE :: OP_PUSHDATA(encodeNumber(32)) :: OP_EQUALVERIFY ::
|
||||
OP_HASH160 :: OP_DUP ::
|
||||
OP_PUSHDATA(ripemd160(rhash)) :: OP_EQUAL ::
|
||||
OP_IF ::
|
||||
OP_HASH160 :: OP_DUP ::
|
||||
OP_PUSHDATA(ripemd160(rhash)) :: OP_EQUAL ::
|
||||
OP_IF ::
|
||||
OP_PUSHDATA(encodeNumber(reltimeout)) :: OP_CHECKSEQUENCEVERIFY :: OP_2DROP :: OP_PUSHDATA(ourkey) ::
|
||||
OP_ELSE ::
|
||||
OP_ELSE ::
|
||||
OP_PUSHDATA(ripemd160(commit_revoke)) :: OP_EQUAL ::
|
||||
OP_NOTIF ::
|
||||
OP_PUSHDATA(encodeNumber(abstimeout)) :: OP_CHECKLOCKTIMEVERIFY :: OP_DROP ::
|
||||
OP_PUSHDATA(encodeNumber(abstimeout)) :: OP_CHECKLOCKTIMEVERIFY :: OP_DROP ::
|
||||
OP_ENDIF ::
|
||||
OP_PUSHDATA(theirkey) ::
|
||||
OP_ENDIF ::
|
||||
OP_CHECKSIG :: Nil
|
||||
OP_ENDIF ::
|
||||
OP_CHECKSIG :: Nil
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV}
|
||||
import org.bouncycastle.jce.ECNamedCurveTable
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 27/10/2015.
|
||||
*/
|
||||
@ -148,11 +150,13 @@ object LightningCrypto {
|
||||
|
||||
case class KeyPair(pub: BinaryData, priv: BinaryData)
|
||||
|
||||
lazy val rand = new SecureRandom()
|
||||
// see http://bugs.java.com/view_bug.do?bug_id=6521844
|
||||
//lazy val random = SecureRandom.getInstanceStrong
|
||||
lazy val random = new Random()
|
||||
|
||||
def randomKeyPair(): KeyPair = {
|
||||
val key = new Array[Byte](32)
|
||||
rand.nextBytes(key)
|
||||
random.nextBytes(key)
|
||||
KeyPair(pub = Crypto.publicKeyFromPrivateKey(key :+ 0x01.toByte), priv = key)
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import java.security.SecureRandom
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto}
|
||||
import fr.acinq.eclair.crypto.LightningCrypto._
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 14/10/2015.
|
||||
@ -86,12 +88,15 @@ object Onion extends App {
|
||||
Secrets(enckey, hmac, iv, pad_iv)
|
||||
}
|
||||
|
||||
lazy val rand = new SecureRandom()
|
||||
// see http://bugs.java.com/view_bug.do?bug_id=6521844
|
||||
//lazy val random = SecureRandom.getInstanceStrong
|
||||
lazy val random = new Random()
|
||||
|
||||
|
||||
def generatePrivateKey(): BinaryData = {
|
||||
val key = new Array[Byte](32)
|
||||
do {
|
||||
rand.nextBytes(key)
|
||||
random.nextBytes(key)
|
||||
} while (Crypto.publicKeyFromPrivateKey(key :+ 0x01.toByte)(0) != 0x02)
|
||||
key
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package fr.acinq.eclair.gui
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import javafx.scene.Scene
|
||||
import javafx.scene.image.{Image, ImageView}
|
||||
import javafx.scene.layout.StackPane
|
||||
import javafx.stage.{Modality, Stage, StageStyle}
|
||||
|
||||
/**
|
||||
* Created by PM on 16/08/2016.
|
||||
*/
|
||||
class DialogGraph(primaryStage: Stage, img: Array[Byte]) extends Stage() {
|
||||
initModality(Modality.NONE)
|
||||
initStyle(StageStyle.UTILITY)
|
||||
initOwner(primaryStage)
|
||||
setTitle("Graph")
|
||||
|
||||
val image = new Image(new ByteArrayInputStream(img))
|
||||
val view = new ImageView(image)
|
||||
|
||||
val pane = new StackPane()
|
||||
pane.getChildren.add(view)
|
||||
|
||||
val scene = new Scene(pane)
|
||||
setScene(scene)
|
||||
}
|
||||
@ -45,15 +45,15 @@ class DialogReceive(primaryStage: Stage, handlers: Handlers) extends Stage() {
|
||||
|
||||
val textAreaPaymentRequest = new TextArea()
|
||||
textAreaPaymentRequest.setWrapText(true)
|
||||
textAreaPaymentRequest.setPrefRowCount(2)
|
||||
textAreaPaymentRequest.setPrefColumnCount(40)
|
||||
textAreaPaymentRequest.setPrefRowCount(3)
|
||||
textAreaPaymentRequest.setPrefColumnCount(50)
|
||||
textAreaPaymentRequest.setEditable(false)
|
||||
grid.add(textAreaPaymentRequest, 1, 2)
|
||||
|
||||
btn.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent): Unit = {
|
||||
Try(textFieldAmountMsat.getText.toLong) match {
|
||||
case Success(amountMsat) => handlers.getPaymentRequest(textFieldAmountMsat.getText.toLong, textAreaPaymentRequest)
|
||||
case Success(amountMsat) => handlers.getPaymentRequest(textFieldAmountMsat.getText.toInt, textAreaPaymentRequest)
|
||||
case _ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package fr.acinq.eclair.gui
|
||||
|
||||
import java.util.Base64
|
||||
import javafx.event.{ActionEvent, EventHandler}
|
||||
import javafx.geometry.{Insets, Pos}
|
||||
import javafx.scene.{Node, Scene}
|
||||
@ -42,8 +43,8 @@ class DialogSend(primaryStage: Stage, handlers: Handlers) extends Stage() {
|
||||
val btn = new Button("Send")
|
||||
btn.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent): Unit = {
|
||||
val Array(nodeId, amount, hash) = textAreaPaymentRequest.getText.split(":")
|
||||
handlers.send(nodeId, hash, amount)
|
||||
val paymentRequest = lightning.payment_request.parseFrom(Base64.getDecoder().decode(textAreaPaymentRequest.getText))
|
||||
handlers.send(paymentRequest)
|
||||
event.getSource.asInstanceOf[Node].getScene.getWindow.hide()
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
package fr.acinq.eclair.gui
|
||||
|
||||
import javafx.application.Platform
|
||||
import javafx.embed.swing.SwingNode
|
||||
import javafx.event.{ActionEvent, EventHandler}
|
||||
import javafx.geometry.Orientation
|
||||
import javafx.scene.control.Separator
|
||||
import javafx.stage.Stage
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef}
|
||||
import com.mxgraph.layout.mxCircleLayout
|
||||
import com.mxgraph.swing.mxGraphComponent
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.{Globals, Setup}
|
||||
import fr.acinq.eclair.Setup
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.router.{ChannelDesc, ChannelDiscovered}
|
||||
import org.jgrapht.ext.JGraphXAdapter
|
||||
import org.jgrapht.graph.{DefaultEdge, SimpleGraph}
|
||||
import fr.acinq.eclair.router.ChannelDiscovered
|
||||
import lightning.channel_open
|
||||
|
||||
|
||||
/**
|
||||
@ -23,12 +18,6 @@ import org.jgrapht.graph.{DefaultEdge, SimpleGraph}
|
||||
*/
|
||||
class GUIUpdater(primaryStage: Stage, helloWorld: MainWindow, setup: Setup) extends Actor with ActorLogging {
|
||||
|
||||
class NamedEdge(val id: BinaryData) extends DefaultEdge {
|
||||
override def toString: String = s"${id.toString.take(8)}..."
|
||||
}
|
||||
val graph = new SimpleGraph[BinaryData, NamedEdge](classOf[NamedEdge])
|
||||
graph.addVertex(Globals.Node.publicKey)
|
||||
|
||||
def receive: Receive = main(Map())
|
||||
|
||||
def main(m: Map[ActorRef, PaneChannel]): Receive = {
|
||||
@ -61,7 +50,7 @@ class GUIUpdater(primaryStage: Stage, helloWorld: MainWindow, setup: Setup) exte
|
||||
}
|
||||
})
|
||||
|
||||
case ChannelChangedState(channel, _, previousState, currentState, currentData) =>
|
||||
case ChannelChangedState(channel, _, _, previousState, currentState, currentData) =>
|
||||
val pane = m(channel)
|
||||
Platform.runLater(new Runnable() {
|
||||
override def run(): Unit = {
|
||||
@ -79,9 +68,9 @@ class GUIUpdater(primaryStage: Stage, helloWorld: MainWindow, setup: Setup) exte
|
||||
}
|
||||
})
|
||||
|
||||
case ChannelDiscovered(ChannelDesc(id, a, b)) =>
|
||||
graph.addVertex(BinaryData(a))
|
||||
graph.addVertex(BinaryData(b))
|
||||
case ChannelDiscovered(channel_open(id, a, b)) => {}
|
||||
/*graph.addVertex(pubkey2bin(a))
|
||||
graph.addVertex(pubkey2bin(b))
|
||||
graph.addEdge(a, b, new NamedEdge(id))
|
||||
val jgxAdapter = new JGraphXAdapter(graph)
|
||||
Platform.runLater(new Runnable() {
|
||||
@ -92,7 +81,7 @@ class GUIUpdater(primaryStage: Stage, helloWorld: MainWindow, setup: Setup) exte
|
||||
lay.execute(jgxAdapter.getDefaultParent())
|
||||
helloWorld.swingNode.setContent(component)
|
||||
}
|
||||
})
|
||||
})*/
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
package fr.acinq.eclair.gui
|
||||
|
||||
|
||||
import java.io._
|
||||
import java.util.Base64
|
||||
import javafx.application.Platform
|
||||
import javafx.scene.control.{TextArea, TextField}
|
||||
import javafx.stage.Stage
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi}
|
||||
import fr.acinq.eclair.io.Client
|
||||
import fr.acinq.eclair.router.CreatePayment
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.io.Client
|
||||
import grizzled.slf4j.Logging
|
||||
import lightning.{channel_open, channel_state_update}
|
||||
|
||||
/**
|
||||
* Created by PM on 16/08/2016.
|
||||
@ -27,9 +30,9 @@ class Handlers(setup: Setup) extends Logging {
|
||||
}
|
||||
}
|
||||
|
||||
def send(nodeId: String, rhash: String, amountMsat: String) = {
|
||||
logger.info(s"sending $amountMsat to $rhash @ $nodeId")
|
||||
paymentSpawner ! CreatePayment(amountMsat.toInt, BinaryData(rhash), BinaryData(nodeId))
|
||||
def send(paymentRequest: lightning.payment_request) = {
|
||||
logger.info(s"sending ${paymentRequest.amountMsat} to ${paymentRequest.rHash} @ ${paymentRequest.nodeId}")
|
||||
paymentSpawner ! paymentRequest
|
||||
}
|
||||
|
||||
def getH(textField: TextField): Unit = {
|
||||
@ -43,15 +46,54 @@ class Handlers(setup: Setup) extends Logging {
|
||||
}
|
||||
}
|
||||
|
||||
def getPaymentRequest(amountMsat: Long, textField: TextArea): Unit = {
|
||||
def getPaymentRequest(amountMsat: Int, textField: TextArea): Unit = {
|
||||
import akka.pattern.ask
|
||||
(paymentHandler ? 'genh).mapTo[BinaryData].map { h =>
|
||||
val future1 = (paymentHandler ? 'genh).mapTo[BinaryData]
|
||||
val future2 = (router ? 'network).mapTo[Seq[channel_open]]
|
||||
val future3 = (router ? 'states).mapTo[Seq[channel_state_update]]
|
||||
val future4 = for {
|
||||
h <- future1
|
||||
channels <- future2
|
||||
states <- future3
|
||||
} yield lightning.payment_request(Globals.Node.publicKey, amountMsat, h, lightning.routing_table(channels), states)
|
||||
future4.map { r =>
|
||||
Platform.runLater(new Runnable() {
|
||||
override def run(): Unit = {
|
||||
textField.setText(s"${Globals.Node.id}:$amountMsat:${h.toString()}")
|
||||
textField.setText(Base64.getEncoder.encodeToString(r.toByteArray))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def exportToDot(file: File): Unit = {
|
||||
import akka.pattern.ask
|
||||
(router ? 'dot).mapTo[String].map(dot => printToFile(file)(writer => writer.write(dot)))
|
||||
}
|
||||
|
||||
def printToFile(f: java.io.File)(op: java.io.FileWriter => Unit) {
|
||||
val p = new FileWriter(f)
|
||||
try {
|
||||
op(p)
|
||||
} finally {
|
||||
p.close()
|
||||
}
|
||||
}
|
||||
|
||||
def displayDot(stage: Stage): Unit = {
|
||||
import akka.pattern.ask
|
||||
(router ? 'dot)
|
||||
.mapTo[String]
|
||||
.map(dot => {
|
||||
import scala.sys.process._
|
||||
val input = new ByteArrayInputStream(dot.getBytes)
|
||||
val output = new ByteArrayOutputStream()
|
||||
"dot -Tpng" #< input #> output !
|
||||
val img = output.toByteArray
|
||||
Platform.runLater(new Runnable() {
|
||||
override def run(): Unit = {
|
||||
new DialogGraph(stage, img).show()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,16 +9,13 @@ import javafx.scene.Scene
|
||||
import javafx.scene.control.TabPane.TabClosingPolicy
|
||||
import javafx.scene.control._
|
||||
import javafx.scene.layout.{BorderPane, HBox, VBox}
|
||||
import javafx.stage.{Stage, WindowEvent}
|
||||
import javafx.stage.FileChooser.ExtensionFilter
|
||||
import javafx.stage.{FileChooser, Stage, WindowEvent}
|
||||
|
||||
import akka.actor.Props
|
||||
import com.mxgraph.swing.mxGraphComponent
|
||||
import fr.acinq.eclair.{Globals, Setup}
|
||||
import fr.acinq.eclair.channel.ChannelEvent
|
||||
import fr.acinq.eclair.router.NetworkEvent
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration.Duration
|
||||
import fr.acinq.eclair.{Globals, Setup}
|
||||
|
||||
|
||||
/**
|
||||
@ -33,9 +30,13 @@ class MainWindow extends Application {
|
||||
val itemConnect = new MenuItem("Open channel")
|
||||
val itemSend = new MenuItem("Pay")
|
||||
val itemReceive = new MenuItem("Receive payment")
|
||||
|
||||
menuChannels.getItems.addAll(itemConnect, new SeparatorMenuItem(), itemSend, itemReceive)
|
||||
menuBar.getMenus().addAll(menuChannels)
|
||||
val menuTools = new Menu("Tools")
|
||||
val itemDotExport = new MenuItem("Export to dot file")
|
||||
val graphDisplay = new MenuItem("Display graph")
|
||||
menuTools.getItems.addAll(itemDotExport, graphDisplay)
|
||||
|
||||
menuBar.getMenus().addAll(menuChannels, menuTools)
|
||||
root.setTop(menuBar)
|
||||
|
||||
val tabChannels = new Tab("Channels")
|
||||
@ -45,8 +46,8 @@ class MainWindow extends Application {
|
||||
tabChannels.setContent(vBoxPane)
|
||||
|
||||
val tabGraph = new Tab("Graph")
|
||||
val swingNode = new SwingNode()
|
||||
tabGraph.setContent(swingNode)
|
||||
//val swingNode = new SwingNode()
|
||||
//tabGraph.setContent(swingNode)
|
||||
|
||||
val paneTab = new TabPane()
|
||||
paneTab.getTabs.addAll(tabChannels, tabGraph)
|
||||
@ -77,11 +78,11 @@ class MainWindow extends Application {
|
||||
hBoxPane.setAlignment(Pos.CENTER_RIGHT)
|
||||
val labelNodeId = new Label(s"Node Id: ${Globals.Node.id}")
|
||||
val separator1 = new Separator(Orientation.VERTICAL)
|
||||
val labelApi = new Label(s"Listening on HTTP ${setup.config.getInt("eclair.api.port")}")
|
||||
val labelApi = new Label(s"HTTP:${setup.config.getInt("eclair.api.port")}")
|
||||
val separator2 = new Separator(Orientation.VERTICAL)
|
||||
val labelServer = new Label(s"Listening on TCP ${setup.config.getInt("eclair.server.port")}")
|
||||
val labelServer = new Label(s"TCP:${setup.config.getInt("eclair.server.port")}")
|
||||
val separator3 = new Separator(Orientation.VERTICAL)
|
||||
val labelBitcoin = new Label(s"Connected to bitcoin-core ${setup.bitcoinVersion} (${setup.chain})")
|
||||
val labelBitcoin = new Label(s"bitcoin-core ${setup.bitcoinVersion} (${setup.chain})")
|
||||
hBoxPane.getChildren.addAll(labelNodeId, separator1, labelApi, separator2, labelServer, separator3, labelBitcoin)
|
||||
root.setBottom(hBoxPane)
|
||||
itemConnect.setOnAction(new EventHandler[ActionEvent] {
|
||||
@ -93,15 +94,29 @@ class MainWindow extends Application {
|
||||
itemReceive.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent): Unit = new DialogReceive(primaryStage, handlers).show()
|
||||
})
|
||||
itemDotExport.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent): Unit = {
|
||||
val fileChooser = new FileChooser()
|
||||
fileChooser.setTitle("Save as")
|
||||
fileChooser.getExtensionFilters.addAll(new ExtensionFilter("DOT File (*.dot)", "*.dot"))
|
||||
val file = fileChooser.showSaveDialog(primaryStage)
|
||||
if (file != null) handlers.exportToDot(file)
|
||||
}
|
||||
})
|
||||
graphDisplay.setOnAction(new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent): Unit = {
|
||||
handlers.displayDot(primaryStage)
|
||||
}
|
||||
})
|
||||
|
||||
def refreshGraph: Unit = {
|
||||
Option(swingNode.getContent) match {
|
||||
/*Option(swingNode.getContent) match {
|
||||
case Some(component: mxGraphComponent) =>
|
||||
component.doLayout()
|
||||
component.repaint()
|
||||
component.refresh()
|
||||
case _ => {}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
tabGraph.setOnSelectionChanged(new EventHandler[Event] {
|
||||
|
||||
@ -1,21 +1,17 @@
|
||||
package fr.acinq.eclair.io
|
||||
|
||||
import javax.crypto.Cipher
|
||||
|
||||
import akka.actor._
|
||||
import akka.io.Tcp.{ErrorClosed, Received, Register, Write}
|
||||
import akka.util.ByteString
|
||||
import com.trueaccord.scalapb.GeneratedMessage
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.bitcoin.{BinaryData, _}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.{Decryptor, Encryptor}
|
||||
import fr.acinq.eclair.crypto.LightningCrypto._
|
||||
import fr.acinq.eclair.crypto.{Decryptor, Encryptor}
|
||||
import lightning._
|
||||
import lightning.pkt.Pkt._
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 27/10/2015.
|
||||
@ -38,7 +34,7 @@ case object IO_NORMAL extends State
|
||||
|
||||
// @formatter:on
|
||||
|
||||
class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef, our_params: OurChannelParams) extends LoggingFSM[State, Data] with Stash {
|
||||
class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef, router: ActorRef, our_params: OurChannelParams, nodeKeys: KeyPair) extends LoggingFSM[State, Data] with Stash {
|
||||
|
||||
val session_key = randomKeyPair()
|
||||
|
||||
@ -49,6 +45,7 @@ class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef
|
||||
them ! Write(ByteString.fromArray(firstMessage))
|
||||
|
||||
def send(encryptor: Encryptor, message: BinaryData): Encryptor = {
|
||||
log.debug(s"encrypting $message")
|
||||
val (encryptor1, ciphertext) = Encryptor.encrypt(encryptor, message)
|
||||
them ! Write(ByteString.fromArray(ciphertext))
|
||||
encryptor1
|
||||
@ -96,8 +93,8 @@ class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef
|
||||
* session_sig is a valid secp256k1 ECDSA signature encoded as a 32-byte big endian R value, followed by a 32-byte big endian S value.
|
||||
* session_sig is the signature of the SHA256 of SHA256 of the receivers node_id, using the secret key corresponding to the sender's node_id.
|
||||
*/
|
||||
val sig: BinaryData = Crypto.encodeSignature(Crypto.sign(Crypto.hash256(their_session_key), Globals.Node.privateKey))
|
||||
val our_auth = pkt(Auth(lightning.authenticate(Globals.Node.publicKey, bin2signature(sig))))
|
||||
val sig: BinaryData = Crypto.encodeSignature(Crypto.sign(Crypto.hash256(their_session_key), nodeKeys.priv))
|
||||
val our_auth = pkt(Auth(lightning.authenticate(nodeKeys.pub, bin2signature(sig))))
|
||||
|
||||
val encryptor = Encryptor(sending_key, 0)
|
||||
val decryptor = Decryptor(receiving_key, 0)
|
||||
@ -110,7 +107,7 @@ class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef
|
||||
|
||||
when(IO_WAITING_FOR_AUTH) {
|
||||
case Event(Received(chunk), s@SessionData(theirpub, decryptor, encryptor)) =>
|
||||
log.debug(s"received chunk=${BinaryData(chunk)}")
|
||||
log.debug(s"decrypting ${BinaryData(chunk)}")
|
||||
val decryptor1 = Decryptor.add(decryptor, chunk)
|
||||
decryptor1.bodies.headOption match {
|
||||
case None => stay using s.copy(decryptor = decryptor1)
|
||||
@ -130,13 +127,13 @@ class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef
|
||||
|
||||
when(IO_NORMAL) {
|
||||
case Event(Received(chunk), n@Normal(channel, s@SessionData(theirpub, decryptor, encryptor))) =>
|
||||
log.debug(s"received chunk=${BinaryData(chunk)}")
|
||||
log.debug(s"decrypting ${BinaryData(chunk)}")
|
||||
val decryptor1 = Decryptor.add(decryptor, chunk)
|
||||
decryptor1.bodies.map(plaintext => {
|
||||
val packet = pkt.parseFrom(plaintext)
|
||||
self ! packet
|
||||
})
|
||||
stay using Normal(channel, s.copy(decryptor = decryptor1.copy(header = None, bodies = Vector.empty[BinaryData])))
|
||||
stay using Normal(channel, s.copy(decryptor = decryptor1.copy(bodies = Vector.empty[BinaryData])))
|
||||
|
||||
case Event(packet: pkt, n@Normal(channel, s@SessionData(theirpub, decryptor, encryptor))) =>
|
||||
log.debug(s"receiving $packet")
|
||||
@ -153,6 +150,10 @@ class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef
|
||||
case CloseShutdown(o) => channel ! o
|
||||
case CloseSignature(o) => channel ! o
|
||||
case Error(o) => channel ! o
|
||||
case NeighborHello(o) => router ! o
|
||||
case NeighborUpdate(o) => router ! o
|
||||
case NeighborReset(o) => router ! o
|
||||
case NeighborOnion(o) => router ! o
|
||||
}
|
||||
stay
|
||||
|
||||
@ -170,6 +171,10 @@ class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef
|
||||
case o: close_shutdown => pkt(CloseShutdown(o))
|
||||
case o: close_signature => pkt(CloseSignature(o))
|
||||
case o: error => pkt(Error(o))
|
||||
case o: neighbor_hello => pkt(NeighborHello(o))
|
||||
case o: neighbor_update => pkt(NeighborUpdate(o))
|
||||
case o: neighbor_reset => pkt(NeighborReset(o))
|
||||
case o: neighbor_onion => pkt(NeighborOnion(o))
|
||||
}
|
||||
log.debug(s"sending $packet")
|
||||
val encryptor1 = send(encryptor, packet)
|
||||
@ -194,63 +199,10 @@ class AuthHandler(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef
|
||||
|
||||
object AuthHandler {
|
||||
|
||||
def props(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef, our_params: OurChannelParams) = Props(new AuthHandler(them, blockchain, paymentHandler, our_params))
|
||||
def props(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef, router: ActorRef, our_params: OurChannelParams): Props =
|
||||
props(them, blockchain, paymentHandler, router, our_params, KeyPair(priv = Globals.Node.privateKey, pub = Globals.Node.publicKey))
|
||||
|
||||
case class Secrets(aes_key: BinaryData, hmac_key: BinaryData, aes_iv: BinaryData)
|
||||
def props(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef, router: ActorRef, our_params: OurChannelParams, nodeKeys: KeyPair): Props =
|
||||
Props(new AuthHandler(them, blockchain, paymentHandler, router, our_params, nodeKeys))
|
||||
|
||||
def generate_secrets(ecdh_key: BinaryData, pub: BinaryData): Secrets = {
|
||||
val aes_key = Crypto.sha256(ecdh_key ++ pub :+ 0x00.toByte).take(16)
|
||||
val hmac_key = Crypto.sha256(ecdh_key ++ pub :+ 0x01.toByte)
|
||||
val aes_iv = Crypto.sha256(ecdh_key ++ pub :+ 0x02.toByte).take(16)
|
||||
Secrets(aes_key, hmac_key, aes_iv)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds up to a factor of 16
|
||||
*
|
||||
* @param l
|
||||
* @return
|
||||
*/
|
||||
def round16(l: Int) = l % 16 match {
|
||||
case 0 => l
|
||||
case x => l + 16 - x
|
||||
}
|
||||
|
||||
def writeMsg(msg: pkt, secrets: Secrets, cipher: Cipher, totlen_prev: Long): (BinaryData, Long) = {
|
||||
val buf = pkt.toByteArray(msg)
|
||||
val enclen = round16(buf.length)
|
||||
val enc = cipher.update(buf.padTo(enclen, 0x00: Byte))
|
||||
val totlen = totlen_prev + buf.length
|
||||
val totlen_bin = Protocol.writeUInt64(totlen)
|
||||
(hmac256(secrets.hmac_key, totlen_bin ++ enc) ++ totlen_bin ++ enc, totlen)
|
||||
}
|
||||
|
||||
/**
|
||||
* splits buffer in separate msg
|
||||
*
|
||||
* @param data raw data received (possibly one, multiple or fractions of messages)
|
||||
* @param totlen_prev
|
||||
* @param f will be applied to full messages
|
||||
* @return rest of the buffer (incomplete msg)
|
||||
*/
|
||||
@tailrec
|
||||
def split(data: BinaryData, secrets: Secrets, cipher: Cipher, totlen_prev: Long, f: BinaryData => Unit): (BinaryData, Long) = {
|
||||
if (data.length < 32 + 8) (data, totlen_prev)
|
||||
else {
|
||||
val totlen = Protocol.uint64(data.slice(32, 32 + 8))
|
||||
val len = (totlen - totlen_prev).asInstanceOf[Int]
|
||||
val enclen = round16(len)
|
||||
if (data.length < 32 + 8 + enclen) (data, totlen_prev)
|
||||
else {
|
||||
val splitted = data.splitAt(32 + 8 + enclen)
|
||||
val refsig = BinaryData(data.take(32))
|
||||
val payload = BinaryData(data.slice(32, 32 + 8 + enclen))
|
||||
val sig = hmac256(secrets.hmac_key, payload)
|
||||
assert(sig.data.sameElements(refsig), "sig mismatch!")
|
||||
val dec = cipher.update(payload.drop(8).toArray)
|
||||
f(dec.take(len))
|
||||
split(splitted._2, secrets, cipher, totlen_prev + len, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,8 +30,8 @@ class Client(remote: InetSocketAddress, amount: Satoshi, register: ActorRef) ext
|
||||
|
||||
object Client extends App {
|
||||
|
||||
def props(address: InetSocketAddress, amount: Satoshi, register: ActorRef): Props = Props(classOf[Client], address, amount, register)
|
||||
def props(address: InetSocketAddress, amount: Satoshi, register: ActorRef): Props = Props(new Client(address, amount, register))
|
||||
|
||||
def props(host: String, port: Int, amount: Satoshi, register: ActorRef): Props = Props(classOf[Client], new InetSocketAddress(host, port), amount, register)
|
||||
def props(host: String, port: Int, amount: Satoshi, register: ActorRef): Props = Props(new Client(new InetSocketAddress(host, port), amount, register))
|
||||
|
||||
}
|
||||
|
||||
@ -31,9 +31,9 @@ class Server(address: InetSocketAddress, register: ActorRef) extends Actor with
|
||||
|
||||
object Server extends App {
|
||||
|
||||
def props(address: InetSocketAddress, register: ActorRef): Props = Props(classOf[Server], address, register)
|
||||
def props(address: InetSocketAddress, register: ActorRef): Props = Props(new Server(address, register))
|
||||
|
||||
def props(host: String, port: Int, register: ActorRef): Props = Props(classOf[Server], new InetSocketAddress(host, port), register)
|
||||
def props(host: String, port: Int, register: ActorRef): Props = Props(new Server(new InetSocketAddress(host, port), register))
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ class ChannelSelector extends Actor with ActorLogging {
|
||||
|
||||
def main(node2channels: Map[BinaryData, Set[ActorRef]], channel2balance: Map[ActorRef, Long]): Receive = {
|
||||
|
||||
case ChannelChangedState(channel, theirNodeId, _, NORMAL, d: DATA_NORMAL) =>
|
||||
case ChannelChangedState(channel, _, theirNodeId, _, NORMAL, d: DATA_NORMAL) =>
|
||||
val bal = d.commitments.theirCommit.spec.amount_them_msat
|
||||
log.info(s"new channel to $theirNodeId with availableMsat=$bal")
|
||||
val channels = node2channels.get(theirNodeId).getOrElse(Set()) + channel
|
||||
|
||||
@ -0,0 +1,555 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel._
|
||||
import lightning._
|
||||
import lightning.neighbor_onion.Next._
|
||||
import lightning.routing_table_update.Update.{ChannelClose, ChannelOpen}
|
||||
import org.graphstream.algorithm.Dijkstra
|
||||
import org.graphstream.graph.implementations.{MultiGraph, MultiNode}
|
||||
import org.graphstream.graph.{Edge, Node}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Future, Promise}
|
||||
import scala.util.{Random, Try}
|
||||
|
||||
/**
|
||||
* Created by PM on 08/09/2016.
|
||||
*/
|
||||
class FlareRouter(myself: bitcoin_pubkey, radius: Int, beaconCount: Int, ticks: Boolean = true, beaconReactivateCount: Int = 5) extends Actor with ActorLogging {
|
||||
|
||||
val MAX_BEACON_DISTANCE = 100
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelChangedState])
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelSignatureReceived])
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
if (ticks) {
|
||||
context.system.scheduler.schedule(10 seconds, 20 seconds, self, 'tick_beacons)
|
||||
context.system.scheduler.schedule(1 minute, 1 minute, self, 'tick_reset)
|
||||
context.system.scheduler.schedule(1 minute, 1 minute, self, 'tick_subscribe)
|
||||
}
|
||||
|
||||
import FlareRouter._
|
||||
|
||||
override def receive: Receive = main({
|
||||
val g = new MultiGraph(UUID.randomUUID().toString)
|
||||
g.addNode[MultiNode](pubkey2string(myself))
|
||||
g
|
||||
}, null, Nil, Nil, Nil, Set(), Map(), Set(), Map(), 0, Map())
|
||||
|
||||
def main(graph: MultiGraph, dijkstra: Dijkstra, neighbors: List[Neighbor], routingUpdatesBatch: List[routing_table_update], channelUpdatesBatch: List[channel_state_update], beacons: Set[Beacon], channelStates: Map[ChannelOneEnd, channel_state_update], subscribed: Set[bitcoin_pubkey], subscribers: Map[bitcoin_pubkey, Seq[bitcoin_pubkey]], mysequence: Int, promises: Map[sha256_hash, Promise[Seq[channel_state_update]]]): Receive = {
|
||||
case ChannelChangedState(channel, connection, theirNodeId, _, NORMAL, d: DATA_NORMAL) if channel == null || self.path.parent == channel.path.parent.parent || self.path.parent == channel.path.parent.parent.parent =>
|
||||
val stateUpdate = commitments2channelState(mysequence, myself, d.commitments)
|
||||
val neighbor = Neighbor(theirNodeId, stateUpdate.channelId, connection, Nil)
|
||||
val channelOpen = channel_open(neighbor.channel_id, myself, neighbor.node_id)
|
||||
val updates = routing_table_update(ChannelOpen(channelOpen)) :: Nil
|
||||
val (graph1, updates1, dijkstra1) = include(myself, graph, updates, radius, beacons.map(_.id))
|
||||
neighbor.connection ! neighbor_hello(graph2table(graph1))
|
||||
log.debug(s"graph is now ${graph2string(graph1)}")
|
||||
val channelStates1 = channelStates + (ChannelOneEnd(stateUpdate.channelId, stateUpdate.node) -> stateUpdate)
|
||||
log.debug(s"channel states are now ${states2string(channelStates1)}")
|
||||
context.system.scheduler.scheduleOnce(1 second, self, 'tick_updates)
|
||||
context become main(graph1, dijkstra1, neighbors :+ neighbor, routingUpdatesBatch ++ updates1, channelUpdatesBatch :+ stateUpdate, beacons, channelStates1, subscribed, subscribers, mysequence + 1, promises)
|
||||
case ChannelSignatureReceived(channel, commitments) if channel == null || self.path.parent == channel.path.parent.parent || self.path.parent == channel.path.parent.parent.parent =>
|
||||
val stateUpdate = commitments2channelState(mysequence, myself, commitments)
|
||||
val channelStates1 = channelStates + (ChannelOneEnd(stateUpdate.channelId, stateUpdate.node) -> stateUpdate)
|
||||
log.debug(s"channel states are now ${states2string(channelStates1)}")
|
||||
context.system.scheduler.scheduleOnce(1 second, self, 'tick_updates)
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch :+ stateUpdate, beacons, channelStates1, subscribed, subscribers, mysequence + 1, promises)
|
||||
case msg@neighbor_hello(table1) =>
|
||||
log.debug(s"received $msg from $sender")
|
||||
val (graph1, dijkstra1) = merge(myself, graph, table1, radius, beacons.map(_.id))
|
||||
// we send beacon req to every newly discovered node
|
||||
val new_nodes = (graph1.getNodeSet[MultiNode].map(node2pubkey(_)).toSet -- graph.getNodeSet[MultiNode].map(node2pubkey(_)).toSet)
|
||||
for (node <- new_nodes) {
|
||||
log.debug(s"$myself sending beacon_req message to new node $node")
|
||||
val (channels1, route) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, node))
|
||||
send(route, neighbors, neighbor_onion(Req(beacon_req(myself, channels1))))
|
||||
}
|
||||
log.debug(s"graph is now ${graph2string(graph1)}")
|
||||
context become main(graph1, dijkstra1, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates, subscribed, subscribers, mysequence, promises)
|
||||
case msg@neighbor_update(routingTableUpdates, channelStateUpdates) =>
|
||||
log.debug(s"received $msg from $sender")
|
||||
// STEP 1: routing table updates
|
||||
val (graph1, updates1, dijkstra1) = include(myself, graph, routingTableUpdates, radius, beacons.map(_.id))
|
||||
// we send beacon req to every newly discovered node
|
||||
val new_nodes = (graph1.getNodeSet[MultiNode].map(node2pubkey(_)).toSet -- graph.getNodeSet[MultiNode].map(node2pubkey(_)).toSet)
|
||||
for (node <- new_nodes) {
|
||||
log.debug(s"sending beacon_req message to new node $node")
|
||||
val (channels1, route) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, node))
|
||||
send(route, neighbors, neighbor_onion(Req(beacon_req(myself, channels1))))
|
||||
}
|
||||
log.debug(s"graph is now ${graph2string(graph1)}")
|
||||
// STEP 2: channel state updates
|
||||
val channelStateUpdates1 = channelStateUpdates.collect {
|
||||
case u@channel_state_update(channelId, sequence, node, amountMsat, relayFee) if Option(graph.getEdge[Edge](sha2562string(channelId))).isDefined =>
|
||||
channelStates.get(ChannelOneEnd(u.channelId, u.node)) match {
|
||||
case Some(s) if s.sequence < sequence =>
|
||||
log.debug(s"received channel state update #$sequence for channelId=$channelId node=$node amountMsat=$amountMsat relayFee=$relayFee")
|
||||
u
|
||||
case Some(s) =>
|
||||
log.debug(s"ignoring channel state update #$sequence for channelId=$channelId node=$node (we are at sequence #${s.sequence}")
|
||||
""
|
||||
case None =>
|
||||
log.debug(s"received first channel state update #$sequence for channelId=$channelId node=$node amountMsat=$amountMsat relayFee=$relayFee")
|
||||
u
|
||||
}
|
||||
} collect { case u: channel_state_update => u } // TODO ugly!
|
||||
val channelStates1 = channelStateUpdates1.foldLeft(channelStates) {
|
||||
case (states, u) => states + (ChannelOneEnd(u.channelId, u.node) -> u)
|
||||
}
|
||||
log.debug(s"channel states are now ${states2string(channelStates1)}")
|
||||
context.system.scheduler.scheduleOnce(3 second, self, 'tick_updates)
|
||||
context become main(graph1, dijkstra1, neighbors, routingUpdatesBatch ++ updates1, channelUpdatesBatch ++ channelStateUpdates1, beacons, channelStates1, subscribed, subscribers, mysequence, promises)
|
||||
case msg@neighbor_reset(channel_ids) =>
|
||||
log.debug(s"received neighbor_reset from $sender with ${channel_ids.size} channels")
|
||||
val diff = graph2table(graph).channels.filterNot(c => channel_ids.contains(c.channelId)).map(c => routing_table_update(ChannelOpen(c)))
|
||||
log.debug(s"sending back ${diff.size} channels to $sender")
|
||||
sender ! neighbor_update(diff)
|
||||
case 'tick_updates if !routingUpdatesBatch.isEmpty || !channelUpdatesBatch.isEmpty =>
|
||||
// to neighbors
|
||||
val neighbors1 = neighbors.map(neighbor => {
|
||||
val diff = routingUpdatesBatch.filterNot(neighbor.sent.contains(_))
|
||||
if (diff.size > 0 || channelUpdatesBatch.size > 0) {
|
||||
log.debug(s"sending $diff and $channelUpdatesBatch to ${neighbor.node_id}")
|
||||
neighbor.connection ! neighbor_update(diff, channelUpdatesBatch)
|
||||
}
|
||||
neighbor.copy(sent = neighbor.sent ::: diff)
|
||||
})
|
||||
// to suscribers
|
||||
channelUpdatesBatch
|
||||
.map(u => neighbor_onion(DynamicInfo(u)))
|
||||
.foreach(msg => subscribers.foreach(suscriber => send(suscriber._2, neighbors, msg)))
|
||||
context become main(graph, dijkstra, neighbors1, Nil, Nil, beacons, channelStates, subscribed, subscribers, mysequence, promises)
|
||||
case 'tick_updates => // nothing to do
|
||||
case 'tick_reset =>
|
||||
val channel_ids = graph2table(graph).channels.map(_.channelId)
|
||||
for (neighbor <- neighbors) {
|
||||
log.debug(s"sending nighbor_reset message to neighbor $neighbor")
|
||||
neighbor.connection ! neighbor_reset(channel_ids)
|
||||
}
|
||||
case 'tick_beacons =>
|
||||
for (node <- Random.shuffle(graph.getNodeSet[MultiNode].toSet - graph.getNode[MultiNode](pubkey2string(myself))).take(beaconReactivateCount)) {
|
||||
log.debug(s"sending beacon_req message to random node $node")
|
||||
val (channels1, route) = findRoute(dijkstra, getOrAdd(graph, myself), node)
|
||||
send(route, neighbors, neighbor_onion(Req(beacon_req(myself, channels1))))
|
||||
}
|
||||
case 'tick_subscribe =>
|
||||
val subscribed1 = beacons.flatMap(beacon => findRoute(dijkstra, getOrAdd(graph, myself), getOrAdd(graph, beacon.id))._2) - myself
|
||||
val remove = subscribed -- subscribed1
|
||||
val add = subscribed1 -- subscribed
|
||||
if (remove.size > 0) {
|
||||
log.info(s"unsubscribing to ${remove.map(n => pubkey2string(n).take(6)).mkString(",")}")
|
||||
val unsubscribeMsg = neighbor_onion(Unsubscribe(unsubscribe(myself)))
|
||||
remove.foreach(node => send(findRoute(dijkstra, getOrAdd(graph, myself), getOrAdd(graph, node))._2, neighbors, unsubscribeMsg))
|
||||
}
|
||||
if (add.size > 0) {
|
||||
log.info(s"subscribing to ${add.map(n => pubkey2string(n).take(6)).mkString(",")}")
|
||||
add.foreach(node => {
|
||||
val (channels, route) = findRoute(dijkstra, getOrAdd(graph, myself), getOrAdd(graph, node))
|
||||
val subscribeMsg = neighbor_onion(Subscribe(subscribe(myself, channels)))
|
||||
send(route, neighbors, subscribeMsg)
|
||||
})
|
||||
}
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates, subscribed1, subscribers, mysequence, promises)
|
||||
case msg@neighbor_onion(onion) =>
|
||||
(onion: @unchecked) match {
|
||||
case lightning.neighbor_onion.Next.Forward(next) =>
|
||||
//log.debug(s"forwarding $msg to ${next.node}")
|
||||
val neighbor = next.toNode
|
||||
neighbor2channel(neighbor, neighbors) match {
|
||||
case Some(channel) => channel ! next.onion
|
||||
case None => log.error(s"could not find channel for neighbor $neighbor, cannot forward $msg")
|
||||
}
|
||||
case lightning.neighbor_onion.Next.Req(req) => self ! req
|
||||
case lightning.neighbor_onion.Next.Ack(ack) => self ! ack
|
||||
case lightning.neighbor_onion.Next.Subscribe(subscribe) => self ! subscribe
|
||||
case lightning.neighbor_onion.Next.Unsubscribe(unsubscribe) => self ! unsubscribe
|
||||
case lightning.neighbor_onion.Next.DynamicInfo(dynamic_info) => self ! dynamic_info
|
||||
case lightning.neighbor_onion.Next.ChannelStatesRequest(req) => self ! req
|
||||
case lightning.neighbor_onion.Next.ChannelStatesResponse(res) => self ! res
|
||||
}
|
||||
case msg@beacon_req(origin, channels) =>
|
||||
log.debug(s"received beacon_req msg from $origin with channels=${channels2string(channels)}")
|
||||
// adding routes to my table so that I can reply to the origin
|
||||
val updates = channels.map(channel => routing_table_update(ChannelOpen(channel))).toList
|
||||
// TODO : maybe we should allow an even larger radius, because their MAX_BEACON_DISTANCE could be larger than ours
|
||||
val (graph1, _, dijkstra1) = include(myself, graph, updates, MAX_BEACON_DISTANCE, Set())
|
||||
val (channels1, route) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, origin))
|
||||
// looking for a strictly better beacon in our neighborhood
|
||||
val nodes: Set[bitcoin_pubkey] = graph.getNodeSet[MultiNode].map(n => node2pubkey(n)).toSet - origin - myself
|
||||
val distances = nodes.map(node => (node, distance(origin, node)))
|
||||
val distance2me = distance(origin, myself)
|
||||
val selected = distances
|
||||
.filter(_._2 < distance2me).toList
|
||||
.sortBy(_._2).map(_._1)
|
||||
.map(alternate => {
|
||||
val (channels2, _) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, alternate))
|
||||
(alternate, channels2, channels1.size + channels2.size)
|
||||
})
|
||||
selected.headOption match {
|
||||
// order matters!
|
||||
case Some((alternate, channels2, hops)) if hops > MAX_BEACON_DISTANCE =>
|
||||
log.debug(s"alternate $alternate was better but it is too far from $origin (hops=$hops)")
|
||||
send(route, neighbors, neighbor_onion(Ack(beacon_ack(myself, None, channels1))))
|
||||
case Some((alternate, channels2, hops)) =>
|
||||
// we reply with a better beacon and give them a route
|
||||
// maybe not the shortest route but we want to be on the path (that's in our interest)
|
||||
log.debug(s"recommending alternate $alternate to $origin (hops=$hops)")
|
||||
send(route, neighbors, neighbor_onion(Ack(beacon_ack(myself, Some(alternate), channels1 ++ channels2))))
|
||||
case _ =>
|
||||
// we accept to be their beacon
|
||||
send(route, neighbors, neighbor_onion(Ack(beacon_ack(myself, None, channels1))))
|
||||
}
|
||||
case msg@beacon_ack(origin, alternative_opt, channels) =>
|
||||
log.debug(s"received beacon_ack msg from $origin with alternative=$alternative_opt channels=${channels2string(channels)}")
|
||||
val updates = channels.map(channel => routing_table_update(ChannelOpen(channel))).toList
|
||||
val (graph1, _, dijkstra1) = include(myself, graph, updates, MAX_BEACON_DISTANCE, Set())
|
||||
alternative_opt match {
|
||||
// order matters!
|
||||
case Some(alternative) if distance(myself, alternative) > distance(myself, origin) =>
|
||||
log.warning(s"$origin is lying ! dist(us, alt) > dist(us, origin)")
|
||||
// TODO we should probably blacklist them
|
||||
case Some(alternative) if Option(graph1.getNode[MultiNode](pubkey2string(alternative))).isEmpty =>
|
||||
log.debug(s"the proposed alternative $alternative is not in our graph (probably too far away)")
|
||||
case Some(alternative) =>
|
||||
log.debug(s"sending alternative $alternative a beacon_req")
|
||||
val (channels1, route) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, alternative))
|
||||
send(route, neighbors, neighbor_onion(Req(beacon_req(myself, channels1))))
|
||||
case None => {} // nothing to do
|
||||
}
|
||||
val beacons1 = if (Option(graph1.getNode[MultiNode](pubkey2string(origin))).isEmpty) {
|
||||
log.debug(s"origin $origin is not in our graph (probably too far away)")
|
||||
beacons
|
||||
} else if (beacons.map(_.id).contains(origin)) {
|
||||
log.debug(s"we already have $origin as a beacon")
|
||||
beacons
|
||||
} else if (beacons.size < beaconCount) {
|
||||
log.info(s"adding $origin as a beacon")
|
||||
val (channels1, _) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, origin))
|
||||
beacons + Beacon(origin, distance(myself, origin), channels1.size)
|
||||
} else if (beacons.exists(b => b.distance > distance(myself, origin))) {
|
||||
val deprecatedBeacon = beacons.find(b => b.distance > distance(myself, origin)).get
|
||||
log.info(s"replacing $deprecatedBeacon by $origin")
|
||||
val (channels1, _) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, origin))
|
||||
beacons - deprecatedBeacon + Beacon(origin, distance(myself, origin), channels1.size)
|
||||
} else {
|
||||
log.info(s"ignoring beacon candidate $origin")
|
||||
beacons
|
||||
}
|
||||
// this will cause old beacons to be removed from the graph
|
||||
val (graph2, _, dijkstra2) = include(myself, graph1, Nil, radius, beacons1.map(_.id))
|
||||
log.debug(s"graph is now ${graph2string(graph2)}")
|
||||
log.debug(s"my beacons are now ${beacons1.map(b => s"${pubkey2string(b.id).take(6)}(${b.hops})").mkString(",")}")
|
||||
context become main(graph2, dijkstra2, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons1, channelStates, subscribed, subscribers, mysequence, promises)
|
||||
case msg@subscribe(origin, channels) =>
|
||||
log.debug(s"received register msg from $origin with channels=${channels2string(channels)}")
|
||||
// adding routes to my table so that I can reply to the origin
|
||||
val updates = channels.map(channel => routing_table_update(ChannelOpen(channel))).toList
|
||||
// TODO : maybe we should allow an even larger radius, because their MAX_BEACON_DISTANCE could be larger than ours
|
||||
val (graph1, _, dijkstra1) = include(myself, graph, updates, MAX_BEACON_DISTANCE, Set())
|
||||
val (_, route) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, origin))
|
||||
// sending my current states to this new subscriber
|
||||
channelStates.values.filter(_.node == myself).foreach(u => send(route, neighbors, neighbor_onion(DynamicInfo(u))))
|
||||
val subscribers1 = subscribers + (origin -> route)
|
||||
log.debug(s"subscribers are now ${subscribers2string(subscribers1)}")
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates, subscribed, subscribers1, mysequence, promises)
|
||||
case msg@unsubscribe(origin) =>
|
||||
log.info(s"received unregister msg from $origin")
|
||||
val subscribers1 = subscribers - origin
|
||||
log.debug(s"subscribers are now ${subscribers2string(subscribers1)}")
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates, subscribed, subscribers1, mysequence, promises)
|
||||
case msg@channel_state_update(channelId, sequence, node, amountMsat, relayFee) =>
|
||||
log.debug(s"received dynamic info from $node for channelId=$channelId")
|
||||
val channelStates1 = if (Option(graph.getEdge[Edge](sha2562string(channelId))).isDefined) {
|
||||
channelStates.get(ChannelOneEnd(channelId, node)) match {
|
||||
case Some(s) if s.sequence < sequence =>
|
||||
log.debug(s"received channel state update #$sequence for channelId=$channelId node=$node amountMsat=$amountMsat relayFee=$relayFee")
|
||||
channelStates + (ChannelOneEnd(channelId, node) -> msg)
|
||||
case Some(s) =>
|
||||
log.debug(s"ignoring channel state update #$sequence for channelId=$channelId node=$node (we are at sequence #${s.sequence}")
|
||||
channelStates
|
||||
case None =>
|
||||
log.debug(s"received first channel state update #$sequence for channelId=$channelId node=$node amountMsat=$amountMsat relayFee=$relayFee")
|
||||
channelStates + (ChannelOneEnd(channelId, node) -> msg)
|
||||
}
|
||||
} else channelStates
|
||||
log.debug(s"channel states are now ${states2string(channelStates1)}")
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates1, subscribed, subscribers, mysequence, promises)
|
||||
case RouteRequest(req, maxIterations) =>
|
||||
log.info(s"received request ${req.rHash} to ${req.nodeId} with maxIterations=$maxIterations")
|
||||
val s = sender
|
||||
val mergedStates = merge(channelStates, req.states)
|
||||
val firstTry = Future(findPaymentRoute(myself, req.nodeId, req.amountMsat, mergedStates.values))
|
||||
val alternate = graph.getNodeIterator[MultiNode]
|
||||
.toList
|
||||
.map(n => node2pubkey(n))
|
||||
.filterNot(_ == myself)
|
||||
.map(node => (node, distance(req.nodeId, node)))
|
||||
.sortBy(_._2)
|
||||
.map(_._1)
|
||||
.take(maxIterations)
|
||||
firstTry.map(r => self ! RouteRequestWip(r, s, req, alternate, mergedStates, 1)).onFailure {
|
||||
case t: Throwable => t.printStackTrace()
|
||||
}
|
||||
case RouteRequestWip(result, s, req, alternate, mergedStates, iterations) =>
|
||||
result match {
|
||||
case _ if result.size > 0 =>
|
||||
log.info(s"found route of size ${result.size} for payment request ${req.rHash} to ${req.nodeId}")
|
||||
s ! RouteResponse(result, iterations)
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates, subscribed, subscribers, mysequence, promises - req.rHash)
|
||||
case _ if alternate.headOption.isDefined =>
|
||||
val node = alternate.head
|
||||
log.info(s"sending route request to alternate ${pubkey2string(node)} iterations=${iterations + 1}")
|
||||
// this will hold alternate node's dynamic infos
|
||||
// TODO: add timeout
|
||||
val promise = Promise[Seq[channel_state_update]]()
|
||||
val (channels, route) = findRoute(dijkstra, getOrAdd(graph, myself), getOrAdd(graph, node))
|
||||
send(route, neighbors, neighbor_onion(ChannelStatesRequest(channel_states_request(myself, req.rHash, channels))))
|
||||
promise.future
|
||||
.map(states => {
|
||||
val mergedStates1 = merge(mergedStates, states)
|
||||
(mergedStates1, findPaymentRoute(myself, req.nodeId, req.amountMsat, mergedStates1.values))
|
||||
})
|
||||
.map(r => self ! RouteRequestWip(r._2, s, req, alternate.drop(1), r._1, iterations + 1))
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates, subscribed, subscribers, mysequence, promises + (req.rHash -> promise))
|
||||
case _ =>
|
||||
log.info(s"cannot find route for payment request ${req.rHash} to ${req.nodeId}")
|
||||
s ! actor.Status.Failure(new RuntimeException("route not found"))
|
||||
context become main(graph, dijkstra, neighbors, routingUpdatesBatch, channelUpdatesBatch, beacons, channelStates, subscribed, subscribers, mysequence, promises - req.rHash)
|
||||
}
|
||||
case msg@channel_states_request(origin, requestId, channels) =>
|
||||
log.debug(s"received channel_states_request from $origin with requestId=$requestId")
|
||||
val updates = channels.map(channel => routing_table_update(ChannelOpen(channel))).toList
|
||||
val (graph1, _, dijkstra1) = include(myself, graph, updates, MAX_BEACON_DISTANCE, Set())
|
||||
val (_, route) = findRoute(dijkstra1, getOrAdd(graph1, myself), getOrAdd(graph1, origin))
|
||||
send(route, neighbors, neighbor_onion(ChannelStatesResponse(channel_states_response(myself, requestId, channelStates.values.toSeq))))
|
||||
case msg@channel_states_response(origin, requestId, states) =>
|
||||
log.debug(s"received channel_states_response from $origin with requestId=$requestId")
|
||||
promises.get(requestId).map(p => p.success(states))
|
||||
case 'network =>
|
||||
sender ! graph2table(graph).channels
|
||||
case 'states =>
|
||||
sender ! channelStates.values.toSeq
|
||||
case 'beacons =>
|
||||
sender ! beacons
|
||||
case 'info =>
|
||||
sender ! FlareInfo(neighbors.map(_.node_id).size, graph.getNodeCount, beacons)
|
||||
case 'dot =>
|
||||
sender ! graph2dot(myself, graph, beacons.map(_.id), channelStates)
|
||||
}
|
||||
|
||||
def send(route: Seq[bitcoin_pubkey], neighbors: List[Neighbor], msg: neighbor_onion): Unit = {
|
||||
require(route.size >= 2, s"$route should be of size >=2 (neighbors=$neighbors msg=$msg)")
|
||||
val onion = buildOnion(route.drop(2), msg)
|
||||
if (route.size < 2) {
|
||||
log.warning(s"invalid route $route")
|
||||
}
|
||||
val neighbor = route(1)
|
||||
neighbor2channel(neighbor, neighbors) match {
|
||||
case Some(actorSelection) => actorSelection ! onion
|
||||
case None => log.error(s"could not find channel for neighbor $neighbor")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
object FlareRouter {
|
||||
|
||||
// @formatter:off
|
||||
case class Neighbor(node_id: bitcoin_pubkey, channel_id: sha256_hash, connection: ActorRef, sent: List[routing_table_update])
|
||||
case class Beacon(id: bitcoin_pubkey, distance: BigInt, hops: Int)
|
||||
case class ChannelOneEnd(channel_id: sha256_hash, node_id: bitcoin_pubkey)
|
||||
case class FlareInfo(neighbors: Int, known_nodes: Int, beacons: Set[Beacon])
|
||||
case class RouteRequest(req: payment_request, maxIterations: Int = 0)
|
||||
case class RouteRequestWip(result: Seq[bitcoin_pubkey], sender: ActorRef, req: payment_request, alternate: Seq[bitcoin_pubkey], wipStates: Map[ChannelOneEnd, channel_state_update], iterations: Int)
|
||||
case class RouteResponse(route: Seq[bitcoin_pubkey], iterations: Int)
|
||||
// @formatter:on
|
||||
|
||||
def commitments2channelState(sequence: Int, myself: bitcoin_pubkey, commitments: Commitments): channel_state_update =
|
||||
// TODO : no fees
|
||||
channel_state_update(commitments.anchorId, sequence, myself, commitments.ourCommit.spec.amount_us_msat.toInt, 0)
|
||||
|
||||
def graph2table(graph: MultiGraph): routing_table =
|
||||
routing_table(graph.getEdgeSet[Edge].map(edge2ChannelDesc(_)).toSeq)
|
||||
|
||||
def node2pubkey(node: Node): bitcoin_pubkey =
|
||||
bin2pubkey(BinaryData(node.getId))
|
||||
|
||||
def edge2ChannelDesc(edge: Edge): channel_open =
|
||||
channel_open(bin2sha256(BinaryData(edge.getId)), bin2pubkey(BinaryData(edge.getSourceNode[MultiNode].getId)), bin2pubkey(BinaryData(edge.getTargetNode[MultiNode].getId)))
|
||||
|
||||
def channelId2sha256(edge: Edge): sha256_hash =
|
||||
bin2sha256(BinaryData(edge.getId))
|
||||
|
||||
def graph2string(graph: MultiGraph): String =
|
||||
s"node_count=${graph.getNodeCount} edges: " + channels2string(graph2table(graph).channels)
|
||||
|
||||
def states2string(states: Map[ChannelOneEnd, channel_state_update]): String =
|
||||
states.values.map(s => s"${pubkey2string(s.node)}@${s.channelId} -> avail: ${s.amountMsat} fee: ${s.relayFee}").mkString(",")
|
||||
|
||||
def subscribers2string(suscribers: Map[bitcoin_pubkey, Seq[bitcoin_pubkey]]): String =
|
||||
s"suscribers=${suscribers.keys.map(n => pubkey2string(n).take(6)).mkString(",")}"
|
||||
|
||||
def pubkey2string(pubkey: bitcoin_pubkey): String =
|
||||
pubkey2bin(pubkey).toString()
|
||||
|
||||
def sha2562string(hash: sha256_hash): String =
|
||||
sha2562bin(hash).toString()
|
||||
|
||||
def channels2string(channels: Seq[channel_open]): String =
|
||||
channels.map(c => s"${pubkey2string(c.nodeA).take(6)}->${pubkey2string(c.nodeB).take(6)}").mkString(" ")
|
||||
|
||||
def graph2dot(myself: bitcoin_pubkey, g: MultiGraph, beacons: Set[bitcoin_pubkey], channelStates: Map[ChannelOneEnd, channel_state_update]): String = {
|
||||
val nodes = g.getNodeIterator[MultiNode].map(node => s""""${node.getId}" [label="${node.getId.take(6)}" tooltip="${node.getId}"];""").mkString("\n")
|
||||
val centerColor = s""""${pubkey2string(myself)}" [color=blue];"""
|
||||
val beaconsColor = beacons.map(beacon => s""""${pubkey2string(beacon)}" [color=red];""").mkString("\n")
|
||||
val edges = g.getEdgeSet[Edge]
|
||||
.flatMap(edge => {
|
||||
val src = ChannelOneEnd(BinaryData(edge.getId), BinaryData(edge.getSourceNode[MultiNode].getId))
|
||||
val srcAvail = channelStates.get(src).map(_.amountMsat)
|
||||
val tgt = ChannelOneEnd(BinaryData(edge.getId), BinaryData(edge.getTargetNode[MultiNode].getId))
|
||||
val tgtAvail = channelStates.get(tgt).map(_.amountMsat)
|
||||
s""" "${pubkey2string(src.node_id)}" -> "${pubkey2string(tgt.node_id)}" [fontsize=8 labeltooltip="${edge.getId}" label=${srcAvail.getOrElse("unknown")}];""" ::
|
||||
s""" "${pubkey2string(tgt.node_id)}" -> "${pubkey2string(src.node_id)}" [fontsize=8 labeltooltip="${edge.getId}" label=${tgtAvail.getOrElse("unknown")}];""" :: Nil
|
||||
}).mkString("\n")
|
||||
|
||||
s"""digraph G {
|
||||
|rankdir=LR;
|
||||
|$nodes
|
||||
|$centerColor
|
||||
|$beaconsColor
|
||||
|$edges
|
||||
|}
|
||||
""".stripMargin
|
||||
}
|
||||
|
||||
def copy(graph: MultiGraph): MultiGraph = {
|
||||
val g1 = new MultiGraph(UUID.randomUUID().toString)
|
||||
graph.getNodeIterator[MultiNode].foreach(node => g1.addNode[MultiNode](node.getId))
|
||||
graph.getEdgeIterator[Edge].foreach(edge => g1.addEdge[Edge](edge.getId, g1.getNode[MultiNode](edge.getSourceNode[MultiNode].getId), g1.getNode[MultiNode](edge.getTargetNode[MultiNode].getId), edge.isDirected))
|
||||
g1
|
||||
}
|
||||
|
||||
def merge(myself: bitcoin_pubkey, graph: MultiGraph, table2: routing_table, radius: Int, beacons: Set[bitcoin_pubkey]): (MultiGraph, Dijkstra) = {
|
||||
val updates = table2.channels.map(c => routing_table_update(ChannelOpen(c)))
|
||||
val (graph1, _, dijkstra1) = include(myself, graph, updates, radius, beacons)
|
||||
(graph1, dijkstra1)
|
||||
}
|
||||
|
||||
def getOrAdd(g: MultiGraph, node: bitcoin_pubkey): MultiNode = {
|
||||
val id = pubkey2string(node)
|
||||
Option(g.getNode[MultiNode](id)).getOrElse(g.addNode[MultiNode](id))
|
||||
}
|
||||
|
||||
def include(myself: bitcoin_pubkey, graph: MultiGraph, updates: Seq[routing_table_update], radius: Int, beacons: Set[bitcoin_pubkey]): (MultiGraph, Seq[routing_table_update], Dijkstra) = {
|
||||
val graph1 = copy(graph)
|
||||
updates.collect {
|
||||
case routing_table_update(ChannelOpen(channel_open(channelId, nodeA, nodeB))) =>
|
||||
val a = getOrAdd(graph1, nodeA)
|
||||
val b = getOrAdd(graph1, nodeB)
|
||||
// optimistic, doesn't matter if it fails
|
||||
Try(graph1.addEdge[Edge](sha2562string(channelId), a, b))
|
||||
case routing_table_update(ChannelClose(channel_close(channelId))) =>
|
||||
// optimistic, doesn't matter if it fails
|
||||
Try(graph1.removeEdge[Edge](sha2562string(channelId)))
|
||||
}
|
||||
val myselfNode = graph1.getNode[MultiNode](pubkey2string(myself))
|
||||
val dijkstra = new Dijkstra()
|
||||
dijkstra.init(graph1)
|
||||
dijkstra.setSource(myselfNode)
|
||||
dijkstra.compute()
|
||||
|
||||
// we whitelist all nodes on the path to beacons
|
||||
val whitelist = beacons.flatMap(beacon => dijkstra.getPathNodes[MultiNode](graph1.getNode[MultiNode](pubkey2string(beacon))))
|
||||
val neighbors = graph1.getNodeSet[MultiNode].toSet -- whitelist - myselfNode
|
||||
// we remove all nodes farther than radius and not on the path to beacons
|
||||
neighbors.collect {
|
||||
// this also eliminates unreachable nodes (infinite path length)
|
||||
case node if (dijkstra.getPathLength(node) > radius) => graph1.removeNode(node)
|
||||
}
|
||||
|
||||
val origChannels = graph.getEdgeSet[Edge].map(e => BinaryData(e.getId)).toSet
|
||||
val newChannels = graph1.getEdgeSet[Edge].map(e => BinaryData(e.getId)).toSet
|
||||
val updates1 = updates.collect {
|
||||
case u@routing_table_update(ChannelOpen(channel_open(channelId, _, _))) if !origChannels.contains(channelId) && newChannels.contains(channelId) => u
|
||||
case u@routing_table_update(ChannelClose(channel_close(channelId))) if origChannels.contains(channelId) && !newChannels.contains(channelId) => u
|
||||
}
|
||||
(graph1, updates1, dijkstra)
|
||||
}
|
||||
|
||||
def findRoute(dijkstra: Dijkstra, source: MultiNode, target: MultiNode): (Seq[channel_open], Seq[bitcoin_pubkey]) = {
|
||||
require(source.getId != target.getId, "trying to find route to self")
|
||||
// make sure that the precomputed dijkstra was for the same source
|
||||
require(dijkstra.getSource[MultiNode] == source)
|
||||
// note: dijkstra path are given in reverse orders
|
||||
val channels = dijkstra.getPathEdges[Edge](target).map(edge => edge2ChannelDesc(edge)).toSeq.reverse
|
||||
val nodes = dijkstra.getPathNodes[MultiNode](target).map(node => node2pubkey(node)).toSeq.reverse
|
||||
(channels, nodes)
|
||||
}
|
||||
|
||||
def buildOnion(nodes: Seq[bitcoin_pubkey], msg: neighbor_onion): neighbor_onion = {
|
||||
nodes.reverse.foldLeft(msg) {
|
||||
case (o, next) => neighbor_onion(Forward(forward(next, o)))
|
||||
}
|
||||
}
|
||||
|
||||
def neighbor2channel(node_id: bitcoin_pubkey, neighbors: List[Neighbor]): Option[ActorRef] =
|
||||
neighbors.find(_.node_id == node_id).map(_.connection)
|
||||
|
||||
def distance(a: BinaryData, b: BinaryData): BigInt = {
|
||||
if (a.length != b.length) {
|
||||
println("foo")
|
||||
}
|
||||
require(a.length == b.length)
|
||||
val c = new Array[Byte](a.length)
|
||||
for (i <- 0 until a.length) c(i) = ((a.data(i) ^ b.data(i)) & 0xff).toByte
|
||||
BigInt(1, c)
|
||||
}
|
||||
|
||||
def merge(states1: Map[ChannelOneEnd, channel_state_update], states2: Seq[channel_state_update]): Map[ChannelOneEnd, channel_state_update] = {
|
||||
states2.foldLeft(states1) {
|
||||
case (m, u) if !m.containsKey(ChannelOneEnd(u.channelId, u.node)) => m + (ChannelOneEnd(u.channelId, u.node) -> u)
|
||||
case (m, u) if m(ChannelOneEnd(u.channelId, u.node)).sequence < u.sequence => m + (ChannelOneEnd(u.channelId, u.node) -> u)
|
||||
case (m, _) => m
|
||||
}
|
||||
}
|
||||
|
||||
def findPaymentRoute(source: bitcoin_pubkey, target: bitcoin_pubkey, amountMsat: Int, states: Iterable[channel_state_update]): Seq[bitcoin_pubkey] = {
|
||||
val g = new MultiGraph(UUID.randomUUID().toString)
|
||||
// unique nodes
|
||||
val nodes = states.map(_.node).toSet
|
||||
nodes.foreach(node => g.addNode[MultiNode](pubkey2string(node)))
|
||||
// edges for which we have the two ends
|
||||
val edges = states
|
||||
.groupBy(_.channelId)
|
||||
.mapValues {
|
||||
case states if states.size == 1 => (states.head.node, null)
|
||||
case states if states.size == 2 => (states.head.node, states.drop(1).head.node)
|
||||
case states if states.size > 2 => throw new RuntimeException("more than 2 nodes attached to same channel, should never happen!")
|
||||
}
|
||||
// let's filter out channels that don't have the required amount available
|
||||
// note: +10 % because fees, which are cumulative so need to be conservative
|
||||
val states1 = states.filter(_.amountMsat > 1.1 * amountMsat)
|
||||
// adding *directed* edges
|
||||
states1.collect {
|
||||
case u if edges(u.channelId)._1 != u.node => g.addEdge[Edge](sha2562string(u.channelId) + pubkey2string(u.node), pubkey2string(u.node), pubkey2string(edges(u.channelId)._1), true)
|
||||
case u if edges(u.channelId)._2 != null && edges(u.channelId)._2 != u.node => g.addEdge[Edge](sha2562string(u.channelId) + pubkey2string(u.node), pubkey2string(u.node), pubkey2string(edges(u.channelId)._2), true)
|
||||
}
|
||||
val dijkstra = new Dijkstra()
|
||||
dijkstra.init(g)
|
||||
dijkstra.setSource(g.getNode[MultiNode](pubkey2string(source)))
|
||||
dijkstra.compute()
|
||||
dijkstra.getPath(g.getNode[MultiNode](pubkey2string(target))).getNodePath.map(node2pubkey(_))
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef}
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.channel.{ChannelChangedState, DATA_NORMAL, NORMAL}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.kitteh.irc.client.library.Client
|
||||
import org.kitteh.irc.client.library.element.ISupportParameter.Network
|
||||
import org.kitteh.irc.client.library.event.channel.ChannelUsersUpdatedEvent
|
||||
import org.kitteh.irc.client.library.event.client.ClientConnectedEvent
|
||||
import org.kitteh.irc.client.library.event.helper.ChannelUserListChangeEvent
|
||||
import org.kitteh.irc.client.library.event.helper.ChannelUserListChangeEvent.Change
|
||||
import org.kitteh.irc.client.library.event.user.PrivateMessageEvent
|
||||
import org.kitteh.irc.lib.net.engio.mbassy.listener.Handler
|
||||
|
||||
import scala.util.Random
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
/**
|
||||
* Created by PM on 25/08/2016.
|
||||
*/
|
||||
class IRCWatcher extends Actor with ActorLogging {
|
||||
|
||||
val ircChannel = "#eclair-gossip"
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelChangedState])
|
||||
|
||||
val client = Client.builder().nick(s"node-${Globals.Node.id.take(8)}").serverHost("irc.freenode.net").build()
|
||||
client.getEventManager().registerEventListener(new NodeIRCListener())
|
||||
client.addChannel(ircChannel)
|
||||
|
||||
// we can't use channel id as nickname because there would be conflicts (a channel has two ends)
|
||||
val rand = new Random()
|
||||
|
||||
override def receive: Receive = main(Map(), Map())
|
||||
|
||||
def main(channels: Map[String, ChannelDesc], localChannels: Map[ActorRef, Client]): Receive = {
|
||||
case ChannelChangedState(channel, theirNodeId, _, NORMAL, d: DATA_NORMAL) =>
|
||||
val channelDesc = ChannelDesc(d.commitments.anchorId, Globals.Node.publicKey, theirNodeId)
|
||||
val channelClient = Client.builder().nick(f"chan-${rand.nextInt(1000000)}%06d").realName(channelDesc.id.toString()).serverHost("irc.freenode.net").build()
|
||||
channelClient.getEventManager().registerEventListener(new ChannelIRCListener(channelDesc))
|
||||
channelClient.addChannel(ircChannel)
|
||||
context become main(channels, localChannels + (channel -> channelClient))
|
||||
|
||||
case ChannelChangedState(channel, theirNodeId, NORMAL, _, _) if localChannels.contains(channel) =>
|
||||
localChannels(channel).shutdown()
|
||||
context become main(channels, localChannels - channel)
|
||||
|
||||
case ('add, nick: String, desc: ChannelDesc) if !channels.contains(nick) && !channels.values.map(_.id).contains(desc.id) =>
|
||||
context.system.eventStream.publish(ChannelDiscovered(desc))
|
||||
context become main(channels + (nick -> desc), localChannels)
|
||||
|
||||
case ('remove, nick: String) if channels.contains(nick) =>
|
||||
context.system.eventStream.publish(ChannelLost(channels(nick)))
|
||||
context become main(channels - nick, localChannels)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NodeIRCListener(implicit context: ActorContext) extends Logging {
|
||||
@Handler
|
||||
def onClientConnected(event: ClientConnectedEvent) {
|
||||
logger.info(s"connected to IRC: ${event.getServerInfo}")
|
||||
}
|
||||
|
||||
@Handler
|
||||
def onChannelUsersUpdated(event: ChannelUsersUpdatedEvent) {
|
||||
logger.debug(s"users updated: $event")
|
||||
event.getChannel.getUsers
|
||||
.filter(_.getNick.startsWith("chan"))
|
||||
.map(chanUser => chanUser.sendMessage("desc"))
|
||||
}
|
||||
|
||||
@Handler
|
||||
def onChannelUserListChangeEvent(event: ChannelUserListChangeEvent) {
|
||||
logger.debug(s"${event.getChange} ${event.getUser}")
|
||||
event.getChange match {
|
||||
case Change.JOIN if event.getUser.getNick.startsWith("chan") => event.getUser.sendMessage("desc")
|
||||
case Change.LEAVE if event.getUser.getNick.startsWith("chan") => context.self ! ('remove, event.getUser.getNick)
|
||||
case _ => {}
|
||||
}
|
||||
}
|
||||
|
||||
val r = """([0-9a-f]{64}): ([0-9a-f]{66})-([0-9a-f]{66})""".r
|
||||
|
||||
@Handler
|
||||
def onPrivateMessage(event: PrivateMessageEvent) {
|
||||
logger.debug(s"got private message: ${event.getMessage}")
|
||||
event.getMessage match {
|
||||
case r(id, a, b) => context.self ! ('add, event.getActor.getNick, ChannelDesc(id, a, b))
|
||||
case _ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelIRCListener(channelDesc: ChannelDesc) extends Logging {
|
||||
@Handler
|
||||
def onClientConnected(event: ClientConnectedEvent) {
|
||||
logger.info(s"channel=${channelDesc.id} connected to IRC: ${event.getServerInfo}")
|
||||
}
|
||||
|
||||
@Handler
|
||||
def onPrivateMessage(event: PrivateMessageEvent) {
|
||||
logger.debug(s"got private message: ${event.getMessage}")
|
||||
event.getMessage match {
|
||||
case "desc" => event.sendReply(s"${channelDesc.id}: ${channelDesc.a}-${channelDesc.b}")
|
||||
case "kill" => event.getClient.shutdown()
|
||||
case _ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import lightning.channel_open
|
||||
|
||||
/**
|
||||
* Created by PM on 26/08/2016.
|
||||
*/
|
||||
trait NetworkEvent
|
||||
|
||||
case class ChannelDiscovered(c: ChannelDesc) extends NetworkEvent
|
||||
case class ChannelDiscovered(c: channel_open) extends NetworkEvent
|
||||
|
||||
case class ChannelLost(c: ChannelDesc) extends NetworkEvent
|
||||
case class ChannelLost(c: channel_open) extends NetworkEvent
|
||||
|
||||
|
||||
@ -5,19 +5,18 @@ import akka.actor.{ActorRef, FSM, LoggingFSM, Props, Status}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
|
||||
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, PaymentFailed, PaymentSent}
|
||||
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, PaymentEvent, PaymentFailed, PaymentSent}
|
||||
import fr.acinq.eclair.router.FlareRouter.{RouteRequest, RouteResponse}
|
||||
import lightning.locktime.Locktime.Blocks
|
||||
import lightning.route_step.Next
|
||||
import lightning.{locktime, route_step, sha256_hash}
|
||||
import lightning._
|
||||
|
||||
// @formatter:off
|
||||
|
||||
case class CreatePayment(amountMsat: Int, h: sha256_hash, targetNodeId: BinaryData)
|
||||
|
||||
sealed trait Data
|
||||
case class WaitingForRequest(currentBlockCount: Long) extends Data
|
||||
case class WaitingForRoute(sender: ActorRef, c: CreatePayment, currentBlockCount: Long) extends Data
|
||||
case class WaitingForChannel(sender: ActorRef,c: CreatePayment, r: Seq[BinaryData], currentBlockCount: Long) extends Data
|
||||
case class WaitingForRoute(sender: ActorRef, c: payment_request, currentBlockCount: Long) extends Data
|
||||
case class WaitingForChannel(sender: ActorRef,c: payment_request, r: Seq[bitcoin_pubkey], currentBlockCount: Long) extends Data
|
||||
case class WaitingForComplete(sender: ActorRef,c: CMD_ADD_HTLC, channel: ActorRef) extends Data
|
||||
|
||||
sealed trait State
|
||||
@ -40,8 +39,8 @@ class PaymentManager(router: ActorRef, selector: ActorRef, initialBlockCount: Lo
|
||||
startWith(WAITING_FOR_REQUEST, WaitingForRequest(initialBlockCount))
|
||||
|
||||
when(WAITING_FOR_REQUEST) {
|
||||
case Event(c: CreatePayment, WaitingForRequest(currentBlockCount)) =>
|
||||
router ! RouteRequest(Globals.Node.publicKey, c.targetNodeId)
|
||||
case Event(c: payment_request, WaitingForRequest(currentBlockCount)) =>
|
||||
router ! RouteRequest(c)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, currentBlockCount)
|
||||
|
||||
case Event(CurrentBlockCount(currentBlockCount), d: WaitingForRequest) =>
|
||||
@ -49,7 +48,7 @@ class PaymentManager(router: ActorRef, selector: ActorRef, initialBlockCount: Lo
|
||||
}
|
||||
|
||||
when(WAITING_FOR_ROUTE) {
|
||||
case Event(RouteResponse(r), WaitingForRoute(s, c, currentBlockCount)) =>
|
||||
case Event(RouteResponse(r, iterations), WaitingForRoute(s, c, currentBlockCount)) =>
|
||||
selector ! SelectChannelRequest(r.drop(1).head)
|
||||
goto(WAITING_FOR_CHANNEL) using WaitingForChannel(s, c, r, currentBlockCount)
|
||||
|
||||
@ -66,9 +65,8 @@ class PaymentManager(router: ActorRef, selector: ActorRef, initialBlockCount: Lo
|
||||
val next = r.drop(1).head
|
||||
val others = r.drop(2)
|
||||
val route = buildRoute(c.amountMsat, next +: others)
|
||||
val cmd = CMD_ADD_HTLC(route.steps(0).amount, c.h, locktime(Blocks(currentBlockCount.toInt + 100 + route.steps.size - 2)), route.copy(steps = route.steps.tail), commit = true)
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentSent])
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentFailed])
|
||||
val cmd = CMD_ADD_HTLC(route.steps(0).amount, c.rHash, locktime(Blocks(currentBlockCount.toInt + 100 + route.steps.size - 2)), route.copy(steps = route.steps.tail), commit = true)
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
|
||||
context.system.eventStream.unsubscribe(self, classOf[CurrentBlockCount])
|
||||
channel ! cmd
|
||||
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, cmd, channel)
|
||||
@ -96,9 +94,9 @@ class PaymentManager(router: ActorRef, selector: ActorRef, initialBlockCount: Lo
|
||||
|
||||
object PaymentManager {
|
||||
|
||||
def props(router: ActorRef, selector: ActorRef, initialBlockCount: Long) = Props(classOf[PaymentManager], router, selector, initialBlockCount)
|
||||
def props(router: ActorRef, selector: ActorRef, initialBlockCount: Long) = Props(new PaymentManager(router, selector, initialBlockCount))
|
||||
|
||||
def buildRoute(finalAmountMsat: Int, nodeIds: Seq[BinaryData]): lightning.route = {
|
||||
def buildRoute(finalAmountMsat: Int, nodeIds: Seq[bitcoin_pubkey]): lightning.route = {
|
||||
|
||||
// TODO : use actual fee parameters that are specific to each node
|
||||
def fee(amountMsat: Int) = nodeFee(Globals.base_fee, Globals.proportional_fee, amountMsat).toInt
|
||||
|
||||
@ -2,6 +2,7 @@ package fr.acinq.eclair.router
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
|
||||
import lightning.payment_request
|
||||
|
||||
/**
|
||||
* Created by PM on 29/08/2016.
|
||||
@ -14,7 +15,7 @@ class PaymentSpawner(router: ActorRef, selector: ActorRef, initialBlockCount: Lo
|
||||
|
||||
def main(currentBlockCount: Long): Receive = {
|
||||
case CurrentBlockCount(count) => context.become(main(currentBlockCount))
|
||||
case c: CreatePayment =>
|
||||
case c: payment_request =>
|
||||
val payFsm = context.actorOf(PaymentManager.props(router, selector, initialBlockCount))
|
||||
payFsm forward c
|
||||
}
|
||||
@ -22,5 +23,5 @@ class PaymentSpawner(router: ActorRef, selector: ActorRef, initialBlockCount: Lo
|
||||
}
|
||||
|
||||
object PaymentSpawner {
|
||||
def props(router: ActorRef, selector: ActorRef, initialBlockCount: Long) = Props(classOf[PaymentSpawner], router, selector, initialBlockCount)
|
||||
def props(router: ActorRef, selector: ActorRef, initialBlockCount: Long) = Props(new PaymentSpawner(router, selector, initialBlockCount))
|
||||
}
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, Status}
|
||||
import akka.pattern.pipe
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import org.jgrapht.alg.DijkstraShortestPath
|
||||
import org.jgrapht.graph.{DefaultEdge, SimpleGraph}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* Created by PM on 24/05/2016.
|
||||
*/
|
||||
|
||||
class Router extends Actor with ActorLogging {
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[NetworkEvent])
|
||||
|
||||
import Router._
|
||||
|
||||
import ExecutionContext.Implicits.global
|
||||
|
||||
def receive: Receive = main(Map())
|
||||
|
||||
def main(channels: Map[BinaryData, ChannelDesc]): Receive = {
|
||||
case ChannelDiscovered(c) =>
|
||||
log.info(s"added channel ${c.id} to available routes")
|
||||
context become main(channels + (c.id -> c))
|
||||
case ChannelLost(c) =>
|
||||
log.info(s"removed channel ${c.id} from available routes")
|
||||
context become main(channels - c.id)
|
||||
case 'network => sender ! channels.values
|
||||
case RouteRequest(start, end) => findRoute(start, end, channels) map(RouteResponse(_)) pipeTo sender
|
||||
}
|
||||
}
|
||||
|
||||
object Router {
|
||||
|
||||
def findRouteDijkstra(myNodeId: BinaryData, targetNodeId: BinaryData, channels: Map[BinaryData, ChannelDesc]): Seq[BinaryData] = {
|
||||
class NamedEdge(val id: BinaryData) extends DefaultEdge
|
||||
val g = new SimpleGraph[BinaryData, NamedEdge](classOf[NamedEdge])
|
||||
channels.values.foreach(x => {
|
||||
g.addVertex(x.a)
|
||||
g.addVertex(x.b)
|
||||
g.addEdge(x.a, x.b, new NamedEdge(x.id))
|
||||
})
|
||||
Option(new DijkstraShortestPath(g, myNodeId, targetNodeId).getPath) match {
|
||||
case Some(path) => {
|
||||
val vertices = path.getEdgeList.foldLeft(List(path.getStartVertex)) {
|
||||
case (rest :+ v, edge) if g.getEdgeSource(edge) == v => rest :+ v :+ g.getEdgeTarget(edge)
|
||||
case (rest :+ v, edge) if g.getEdgeTarget(edge) == v => rest :+ v :+ g.getEdgeSource(edge)
|
||||
}
|
||||
vertices
|
||||
}
|
||||
case None => throw new RuntimeException("route not found")
|
||||
}
|
||||
}
|
||||
|
||||
def findRoute(myNodeId: BinaryData, targetNodeId: BinaryData, channels: Map[BinaryData, ChannelDesc])(implicit ec: ExecutionContext): Future[Seq[BinaryData]] = Future {
|
||||
findRouteDijkstra(myNodeId, targetNodeId, channels)
|
||||
}
|
||||
}
|
||||
|
||||
case class ChannelDesc(id: BinaryData, a: BinaryData, b: BinaryData)
|
||||
|
||||
case class RouteRequest(source: BinaryData, target: BinaryData)
|
||||
|
||||
case class RouteResponse(route: Seq[BinaryData])
|
||||
@ -0,0 +1,47 @@
|
||||
package fr.acinq.protos
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import fr.acinq.bitcoin.{BinaryData, BitcoinJsonRPCClient, Satoshi, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain.ExtendedBitcoinClient
|
||||
import fr.acinq.eclair.blockchain.peer.{NewBlock, NewTransaction}
|
||||
import fr.acinq.eclair.channel.Scripts
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 26/04/2016.
|
||||
*/
|
||||
class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinClient(new BitcoinJsonRPCClient("", "", "", 0)) {
|
||||
|
||||
client.client.close()
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
system.scheduler.schedule(100 milliseconds, 100 milliseconds, new Runnable {
|
||||
override def run(): Unit = system.eventStream.publish(NewBlock(null)) // blocks are not actually interpreted
|
||||
})
|
||||
|
||||
override def makeAnchorTx(ourCommitPub: BinaryData, theirCommitPub: BinaryData, amount: Satoshi)(implicit ec: ExecutionContext): Future[(Transaction, Int)] = {
|
||||
val anchorTx = Transaction(version = 1,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = TxOut(amount, Scripts.anchorPubkeyScript(ourCommitPub, theirCommitPub)) :: Nil,
|
||||
lockTime = 0
|
||||
)
|
||||
Future.successful((anchorTx, 0))
|
||||
}
|
||||
|
||||
override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = {
|
||||
system.eventStream.publish(NewTransaction(tx))
|
||||
Future.successful(tx.txid.toString())
|
||||
}
|
||||
|
||||
override def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] = Future.successful(Some(10))
|
||||
|
||||
override def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] = ???
|
||||
|
||||
override def fundTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = ???
|
||||
|
||||
override def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = ???
|
||||
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package fr.acinq.protos.flare
|
||||
|
||||
import org.graphstream.algorithm.generator.WattsStrogatzGenerator
|
||||
import org.graphstream.graph.{Edge, Node}
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
object GenGraph extends App {
|
||||
var n = 0
|
||||
var k = 4
|
||||
var p = 0.2
|
||||
var show = false
|
||||
|
||||
def usage = {
|
||||
println(
|
||||
"""
|
||||
|options:
|
||||
|-n number of nodes
|
||||
|-k number of neighbours per node (default = 4)
|
||||
|-p rewiring probability (default = 0.2)
|
||||
|-show display thre resulting graph
|
||||
""".stripMargin)
|
||||
}
|
||||
|
||||
def parse(arguments: List[String]): Unit = arguments match {
|
||||
case "-help" :: _ => usage; System.exit(1)
|
||||
case "-n" :: value :: tail => n = value.toInt; parse(tail)
|
||||
case "-k" :: value :: tail => k = value.toInt; parse(tail)
|
||||
case "-p" :: value :: tail => p = value.toDouble; parse(tail)
|
||||
case "-show" :: tail => show = true; parse(tail)
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
parse(args.toList)
|
||||
if (n == 0) {
|
||||
usage
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
def genGraph(n: Int, k: Int, p: Double): SingleGraph = {
|
||||
val graph = new SingleGraph("This is a small world!")
|
||||
val gen = new WattsStrogatzGenerator(n, k, p)
|
||||
|
||||
gen.addSink(graph)
|
||||
gen.begin()
|
||||
while (gen.nextEvents()) {}
|
||||
gen.end()
|
||||
graph
|
||||
}
|
||||
|
||||
def convert(graph: SingleGraph): Map[Int, Set[Int]] = {
|
||||
graph.getNodeSet[Node].map(node => {
|
||||
val neighbours = node.getEachEdge[Edge].map(e => e.getOpposite[Node](node)).map(_.getIndex)
|
||||
(node.getIndex -> neighbours.toSet)
|
||||
}).toMap
|
||||
}
|
||||
|
||||
val graph = genGraph(n, k, p)
|
||||
val edgemap = convert(graph)
|
||||
(0 until n).foreach(i => {
|
||||
print(i)
|
||||
edgemap(i).foreach(j => print(s" $j"))
|
||||
println()
|
||||
})
|
||||
|
||||
if (show) graph.display(false)
|
||||
}
|
||||
329
eclair-node/src/main/scala/fr/acinq/protos/flare/Simulator.scala
Normal file
329
eclair-node/src/main/scala/fr/acinq/protos/flare/Simulator.scala
Normal file
@ -0,0 +1,329 @@
|
||||
package fr.acinq.protos.flare
|
||||
|
||||
import java.io.{BufferedOutputStream, File, FileOutputStream, FileWriter}
|
||||
import java.math.BigInteger
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props, Stash}
|
||||
import akka.io.Tcp.{Received, Write}
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet, Satoshi}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.PeerWatcher
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.LightningCrypto.KeyPair
|
||||
import fr.acinq.eclair.io.AuthHandler
|
||||
import fr.acinq.eclair.router.{ChannelSelector, FlareRouter, PaymentManager}
|
||||
import fr.acinq.eclair.router.FlareRouter.{FlareInfo, RouteRequest, RouteResponse}
|
||||
import fr.acinq.protos.TestBitcoinClient
|
||||
import lightning._
|
||||
import lightning.locktime.Locktime.Blocks
|
||||
import org.apache.commons.math3.stat.descriptive.SummaryStatistics
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
||||
import org.graphstream.graph.{Edge, Node}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.io.{Source, StdIn}
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by fabrice on 19/09/16.
|
||||
*/
|
||||
object Simulator extends App {
|
||||
var n = 0
|
||||
var k = 4
|
||||
var radius = 2
|
||||
var maxBeacons = 5
|
||||
var beaconReactivateCount = 5
|
||||
var p = 0.2
|
||||
var filename = ""
|
||||
var gen = false
|
||||
var save = false
|
||||
|
||||
def parse(arguments: List[String]): Unit = arguments match {
|
||||
case "-n" :: value :: tail => n = value.toInt; parse(tail)
|
||||
case "-k" :: value :: tail => k = value.toInt; parse(tail)
|
||||
case "-p" :: value :: tail => p = value.toDouble; parse(tail)
|
||||
case "-r" :: value :: tail => radius = value.toInt; parse(tail)
|
||||
case "-nb" :: value :: tail => maxBeacons = value.toInt; parse(tail)
|
||||
case "-nbr" :: value :: tail => beaconReactivateCount = value.toInt; parse(tail)
|
||||
case "-gen" :: tail => gen = true; parse(tail)
|
||||
case "-save" :: tail => save = true; parse(tail)
|
||||
case value :: tail => filename = value; parse(tail)
|
||||
case Nil => ()
|
||||
}
|
||||
|
||||
parse(args.toList)
|
||||
|
||||
/**
|
||||
* read links from a text file. The format of the file is:
|
||||
* - node ids are integer from 0 to N -1 where N is the number of nodes
|
||||
* - for each node n there is a line tat starts with n followed by the list of all the other nodes it is connected to
|
||||
*
|
||||
* @param filename file name
|
||||
* @return a list of links
|
||||
*/
|
||||
def readLinks(filename: String): Map[Int, Set[Int]] = {
|
||||
Source.fromFile(filename).getLines().toList.filterNot(_.startsWith("#")).map(line => {
|
||||
val a = line.split(" ").map(_.toInt)
|
||||
a.head -> a.tail.toSet
|
||||
}).toMap
|
||||
}
|
||||
|
||||
println(s"flare parameters: radius=$radius beacons=$maxBeacons beacon-reactivate-count=$beaconReactivateCount")
|
||||
val links = (gen, filename) match {
|
||||
case (true, "") =>
|
||||
println(s"running simulation with a generated graph(n = $n, k=$k, p=$p)")
|
||||
GenGraph.convert(GenGraph.genGraph(n, k, p))
|
||||
case (true, _) => throw new IllegalArgumentException("you cannot specify a file name if you use the -gen option")
|
||||
case (false, "") => throw new IllegalArgumentException("you must specify a file name or use the -gen option")
|
||||
case (false, _) =>
|
||||
println(s"running simulation of $filename")
|
||||
readLinks(filename)
|
||||
}
|
||||
|
||||
def saveAsDot(graph: Map[Int, Set[Int]], filename: String): Unit = {
|
||||
val writer = new FileWriter(new File(filename))
|
||||
writer.append("graph G {\n")
|
||||
graph.foreach {
|
||||
case (source, targets) => targets.filter(_ > source).foreach(target => writer.append(s""""$source" -- "$target"\n"""))
|
||||
}
|
||||
writer.append("}\n")
|
||||
writer.close()
|
||||
}
|
||||
|
||||
def saveAsText(graph: Map[Int, Set[Int]], filename: String): Unit = {
|
||||
val writer = new FileWriter(new File(filename))
|
||||
graph.foreach {
|
||||
case (source, targets) =>
|
||||
writer.append(s"$source")
|
||||
targets.filter(_ > source).foreach(target => writer.append(s" $target"))
|
||||
writer.append("\n")
|
||||
}
|
||||
writer.close()
|
||||
}
|
||||
|
||||
// to display the graph use the circo layout: xdot -f circo simulator.dot
|
||||
if (save) {
|
||||
saveAsDot(links, "simulator.dot")
|
||||
saveAsText(links, "simulator.txt")
|
||||
}
|
||||
|
||||
val fullGraph = new SingleGraph("simulator")
|
||||
|
||||
def getOrAddNode(i: Int): Node = Option(fullGraph.getNode[Node](i.toString)).getOrElse(fullGraph.addNode[Node](i.toString))
|
||||
|
||||
links.foreach {
|
||||
case (source, targets) =>
|
||||
val srcNode = getOrAddNode(source)
|
||||
targets.filter(_ > source).foreach(target => {
|
||||
val tgtNode = getOrAddNode(target)
|
||||
fullGraph.addEdge[Edge](s"$source-$target", srcNode, tgtNode)
|
||||
})
|
||||
}
|
||||
|
||||
val maxId = links.keySet.max
|
||||
val nodeIds = (0 to maxId).map(nodeId)
|
||||
val indexMap = (0 to maxId).map(i => nodeIds(i) -> i).toMap
|
||||
|
||||
val system = ActorSystem("mySystem")
|
||||
val nodes = (0 to maxId).map(i => system.actorOf(Props(new MyNode(i)), s"node$i"))
|
||||
//val routers = (0 to maxId).map(i => nodes(i).actorOf(Props(new FlareRouter(nodeIds(i), radius, maxBeacons, false)), i.toString()))
|
||||
|
||||
def keyPair(i: Int) = {
|
||||
val priv = Crypto.sha256(i.toString.getBytes)
|
||||
val pub = Crypto.publicKeyFromPrivateKey(priv :+ 1.toByte)
|
||||
KeyPair(pub = pub, priv = priv)
|
||||
}
|
||||
|
||||
val blockchain = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient()(system), 300)))
|
||||
val paymentHandler = system.settings.config.getString("eclair.payment-handler") match {
|
||||
case "local" => system.actorOf(Props[LocalPaymentHandler], name = "payment-handler")
|
||||
case "noop" => system.actorOf(Props[NoopPaymentHandler], name = "payment-handler")
|
||||
}
|
||||
|
||||
def channelParams(i: Int): OurChannelParams = {
|
||||
val commitPrivKey = Crypto.sha256(i.toString.getBytes) :+ 1.toByte
|
||||
val finalPrivKey = Crypto.sha256(commitPrivKey) :+ 1.toByte
|
||||
val anchorAmount = 100000
|
||||
OurChannelParams(locktime(Blocks(300)), commitPrivKey, finalPrivKey, 1, 10000, Crypto.sha256("alice-seed".getBytes()), Some(Satoshi(anchorAmount)))
|
||||
}
|
||||
|
||||
class MyNode(i: Int) extends Actor with ActorLogging {
|
||||
val router = context.actorOf(Props(new FlareRouter(nodeIds(i), radius, maxBeacons, false)), i.toString())
|
||||
val selector = context.actorOf(Props[ChannelSelector])
|
||||
|
||||
def receive = {
|
||||
case ('connect, node: ActorRef) =>
|
||||
val pipe = context.actorOf(Props[MyPipe])
|
||||
val auth = context.actorOf(AuthHandler.props(pipe, blockchain, paymentHandler, router, channelParams(i), keyPair(i)))
|
||||
pipe ! auth
|
||||
node ! ('accept, pipe)
|
||||
case ('accept, pipe: ActorRef) =>
|
||||
val auth = context.actorOf(AuthHandler.props(pipe, blockchain, paymentHandler, router, channelParams(i).copy(anchorAmount = None), keyPair(i)))
|
||||
pipe ! auth
|
||||
case c: payment_request =>
|
||||
val payFsm = context.actorOf(PaymentManager.props(router, selector, 300))
|
||||
payFsm forward c
|
||||
case t => router forward t
|
||||
}
|
||||
}
|
||||
|
||||
class MyPipe extends Actor with ActorLogging with Stash {
|
||||
|
||||
override def unhandled(message: Any): Unit = {
|
||||
super.unhandled(message)
|
||||
log.warning(s"unhandled message $message")
|
||||
}
|
||||
|
||||
def receive = {
|
||||
case (a: ActorRef, b: ActorRef) =>
|
||||
unstashAll()
|
||||
context become running(a, b)
|
||||
case a: ActorRef => context become waiting(a)
|
||||
case _ => stash()
|
||||
}
|
||||
|
||||
def waiting(a: ActorRef): Receive = {
|
||||
case b: ActorRef =>
|
||||
unstashAll()
|
||||
context become running(a, b)
|
||||
case _ => stash()
|
||||
}
|
||||
|
||||
def running(a: ActorRef, b: ActorRef): Receive = {
|
||||
case akka.io.Tcp.Register(_, _, _) => ()
|
||||
case Write(data, _) if sender == a => b.tell(Received(data), a)
|
||||
case Write(data, _) if sender == b => a.tell(Received(data), b)
|
||||
case msg if sender == a => b forward msg
|
||||
case msg if sender == b => a forward msg
|
||||
}
|
||||
}
|
||||
|
||||
def createChannel(a: Int, b: Int): Unit = {
|
||||
nodes(a) ! ('connect, nodes(b))
|
||||
// routers(a) ! genChannelChangedState(routers(b), nodeIds(b), channelId(nodeIds(a), nodeIds(b)), 1000000000)
|
||||
// routers(b) ! genChannelChangedState(routers(a), nodeIds(a), channelId(nodeIds(a), nodeIds(b)), 1000000000)
|
||||
}
|
||||
|
||||
links.foreach { case (source, targets) => targets.filter(_ > source).foreach(target => createChannel(source, target)) }
|
||||
|
||||
def callToAction: Boolean = {
|
||||
println("'r' => send tick_reset to all actors")
|
||||
println("'b' => send tick_beacons to all actors")
|
||||
println("'s' => send tick_subscribe to all actors")
|
||||
println("'i' => get flare_info from all actors")
|
||||
println("'c' => continue")
|
||||
StdIn.readLine("?") match {
|
||||
case "r" =>
|
||||
for (router <- nodes) router ! 'tick_reset
|
||||
true
|
||||
case "b" =>
|
||||
for (router <- nodes) router ! 'tick_beacons
|
||||
true
|
||||
case "s" =>
|
||||
for (router <- nodes) router ! 'tick_subscribe
|
||||
true
|
||||
case "i" =>
|
||||
implicit val timeout = Timeout(1 minute)
|
||||
val futures = nodes.map(router => (router ? 'info).mapTo[FlareInfo])
|
||||
val results = Await.result(Future.sequence(futures), 30 second)
|
||||
val neighborsStats = new SummaryStatistics()
|
||||
val knownStats = new SummaryStatistics()
|
||||
val beaconsHopsStats = new SummaryStatistics()
|
||||
results.foreach(result => {
|
||||
neighborsStats.addValue(result.neighbors)
|
||||
knownStats.addValue(result.known_nodes)
|
||||
result.beacons.foreach(beacon => beaconsHopsStats.addValue(beacon.hops))
|
||||
})
|
||||
println(f"neighborMin=${neighborsStats.getMin}%.2f neighborMax=${neighborsStats.getMax}%.2f neighborAvg=${neighborsStats.getMean}%.2f neighborVar=${neighborsStats.getVariance}%.2f")
|
||||
println(f"knownMin=${knownStats.getMin}%.2f knownMax=${knownStats.getMax}%.2f knownAvg=${knownStats.getMean}%.2f knownVar=${knownStats.getVariance}%.2f")
|
||||
println(f"beaconHopsMin=${beaconsHopsStats.getMin}%.2f beaconHopsMax=${beaconsHopsStats.getMax}%.2f beaconHopsAvg=${beaconsHopsStats.getMean}%.2f beaconHopsVar=${beaconsHopsStats.getVariance}%.2f")
|
||||
true
|
||||
case "c" => false
|
||||
case x =>
|
||||
println(s"'$x' not supported")
|
||||
callToAction
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
Thread.sleep(10000)
|
||||
} while (callToAction)
|
||||
|
||||
implicit val timeout = Timeout(5 second)
|
||||
|
||||
if (save) {
|
||||
val futures = (0 to maxId).map(i => {
|
||||
val future = for {
|
||||
dot <- (nodes(i) ? 'dot).mapTo[String]
|
||||
} yield printToFile(new File(s"$i.dot"))(writer => writer.write(dot.getBytes))
|
||||
|
||||
future.onFailure {
|
||||
case t: Throwable =>
|
||||
println(s"cannot write routing table for $i: $t")
|
||||
}
|
||||
future
|
||||
})
|
||||
Await.ready(Future.sequence(futures), 15 second)
|
||||
}
|
||||
|
||||
var success = 0
|
||||
var failures = 0
|
||||
val routeStats = new SummaryStatistics()
|
||||
val iterationstats = new SummaryStatistics()
|
||||
for (i <- 0 to maxId) {
|
||||
for (j <- Random.shuffle(i + 1 to maxId).filter(_ != i)) {
|
||||
val future = (for {
|
||||
channels <- (nodes(j) ? 'network).mapTo[Seq[channel_open]]
|
||||
states <- (nodes(j) ? 'states).mapTo[Seq[channel_state_update]]
|
||||
req = payment_request(nodeIds(j), 1, sha256_hash(1, 2, 3, 4), routing_table(channels), states)
|
||||
request = RouteRequest(req, maxIterations = 5)
|
||||
response <- (nodes(i) ? request).mapTo[RouteResponse]
|
||||
} yield {
|
||||
success = success + 1
|
||||
routeStats.addValue(response.route.size)
|
||||
iterationstats.addValue(response.iterations)
|
||||
})
|
||||
.recover {
|
||||
case t: Throwable =>
|
||||
println(s"cannot find route from $i to $j")
|
||||
/*Option(new DijkstraShortestPath(fullGraph, i, j, 100).getPath).foreach(path => {
|
||||
println(path.getEdgeList.map(e => s"${fullGraph.getEdgeSource(e)} -- ${fullGraph.getEdgeTarget(e)}"))
|
||||
})*/
|
||||
failures = failures + 1
|
||||
}
|
||||
Await.ready(future, 15 seconds)
|
||||
if ((success + failures) % 10 == 0) {
|
||||
val successRate = (100 * success) / (success + failures)
|
||||
val progress = 100 * i.toDouble / (maxId - 1)
|
||||
println(f"tested=${success + failures} success=$successRate%.2f%% avgLen=${routeStats.getMean}%.2f maxLen=${routeStats.getMax}%.2f varLen=${routeStats.getVariance}%.2f avgItr=${iterationstats.getMean}%.2f maxItr=${iterationstats.getMax}%.2f varItr=${iterationstats.getVariance}%.2f progress=$progress%.2f%%")
|
||||
}
|
||||
}
|
||||
}
|
||||
system.terminate()
|
||||
|
||||
def channelId(a: BinaryData, b: BinaryData): BinaryData = {
|
||||
if (Scripts.isLess(a, b)) Crypto.sha256(a ++ b) else Crypto.sha256(b ++ a)
|
||||
}
|
||||
|
||||
def nodeId(i: Int): bitcoin_pubkey = keyPair(i).pub
|
||||
|
||||
def genChannelChangedState(them: ActorRef, theirNodeId: BinaryData, channelId: BinaryData, available_amount: Int): ChannelChangedState =
|
||||
ChannelChangedState(null, them, theirNodeId, null, NORMAL, DATA_NORMAL(new Commitments(null, null,
|
||||
OurCommit(0, CommitmentSpec(Set(), 0, 0, 0, available_amount, 0), null),
|
||||
null, null, null, 0L, null, null, null, null) {
|
||||
override def anchorId: BinaryData = channelId // that's the only thing we need
|
||||
}, null, null))
|
||||
|
||||
def printToFile(f: java.io.File)(op: java.io.OutputStream => Unit) {
|
||||
val p = new BufferedOutputStream(new FileOutputStream(f))
|
||||
try {
|
||||
op(p)
|
||||
} finally {
|
||||
p.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@
|
||||
</appender>
|
||||
|
||||
<logger name="com.ning.http" level="INFO"/>
|
||||
<logger name="fr.acinq.eclair.router" level="INFO"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
|
||||
@ -37,8 +37,6 @@ class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinC
|
||||
|
||||
override def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] = Future.successful(Some(10))
|
||||
|
||||
override def isUnspent(txId: String, outputIndex: Int)(implicit ec: ExecutionContext): Future[Boolean] = ???
|
||||
|
||||
override def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] = ???
|
||||
|
||||
override def fundTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = ???
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package fr.acinq.eclair.channel.simulator
|
||||
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
@ -16,8 +15,8 @@ import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.util.Random
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ThroughputSpec extends FunSuite {
|
||||
@ -26,7 +25,7 @@ class ThroughputSpec extends FunSuite {
|
||||
val pipe = system.actorOf(Props[Pipe], "pipe")
|
||||
val blockchain = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)), "blockchain")
|
||||
val paymentHandler = system.actorOf(Props(new Actor() {
|
||||
val random = SecureRandom.getInstanceStrong
|
||||
val random = new Random()
|
||||
|
||||
def generateR(): BinaryData = {
|
||||
val r = Array.fill[Byte](32)(0)
|
||||
@ -60,7 +59,7 @@ class ThroughputSpec extends FunSuite {
|
||||
val latch = new CountDownLatch(2)
|
||||
val listener = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case ChannelChangedState(_, _, _, NORMAL, _) => latch.countDown()
|
||||
case ChannelChangedState(_, _, _, _, NORMAL, _) => latch.countDown()
|
||||
}
|
||||
}), "listener")
|
||||
system.eventStream.subscribe(listener, classOf[ChannelEvent])
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
package fr.acinq.eclair.crypto
|
||||
|
||||
import fr.acinq.eclair.io.AuthHandler
|
||||
import AuthHandler._
|
||||
import fr.acinq.eclair.crypto.LightningCrypto._
|
||||
import lightning.{error, pkt}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
/**
|
||||
* Created by PM on 27/10/2015.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class AuthHandlerSpec extends FunSuite {
|
||||
|
||||
test("encrypt/decrypt loop") {
|
||||
val keypairA = randomKeyPair()
|
||||
val keypairB = randomKeyPair()
|
||||
val secrets = generate_secrets(ecdh(keypairA.pub, keypairB.priv), keypairA.pub)
|
||||
val cipher_in = aesDecryptCipher(secrets.aes_key, secrets.aes_iv)
|
||||
val cipher_out = aesEncryptCipher(secrets.aes_key, secrets.aes_iv)
|
||||
val msg1 = pkt().withError(error(Some("hello")))
|
||||
val (data, totlen) = writeMsg(msg1, secrets, cipher_out, 0)
|
||||
var msg2: pkt = null
|
||||
val (_, totlen2) = split(data, secrets, cipher_in, 0, d => msg2 = pkt.parseFrom(d))
|
||||
assert(msg1 === msg2)
|
||||
}
|
||||
|
||||
test("decrypt msgs received in bulk") {
|
||||
val keypairA = randomKeyPair()
|
||||
val keypairB = randomKeyPair()
|
||||
val secrets = generate_secrets(ecdh(keypairA.pub, keypairB.priv), keypairA.pub)
|
||||
val cipher_in = aesDecryptCipher(secrets.aes_key, secrets.aes_iv)
|
||||
val cipher_out = aesEncryptCipher(secrets.aes_key, secrets.aes_iv)
|
||||
val msgs = for (i <- 0 until 3) yield pkt().withError(error(Some(s"hello #$i!")))
|
||||
val (data0, totlen0) = writeMsg(msgs(0), secrets, cipher_out, 0)
|
||||
val (data1, totlen1) = writeMsg(msgs(1), secrets, cipher_out, totlen0)
|
||||
val (data2, totlen2) = writeMsg(msgs(2), secrets, cipher_out, totlen1)
|
||||
val data = data0 ++ data1 ++ data2
|
||||
var splitted: Seq[pkt] = Nil
|
||||
split(data, secrets, cipher_in, 0, d => splitted = splitted :+ pkt.parseFrom(d))
|
||||
assert(msgs === splitted)
|
||||
}
|
||||
|
||||
}
|
||||
@ -22,7 +22,7 @@ object EncryptorSpec {
|
||||
|
||||
def receive = running(Encryptor(sendingKey, 0), Decryptor(receivinKey, 0))
|
||||
|
||||
def running(encryptor: Encryptor, decryptor: Decryptor) : Receive = {
|
||||
def running(encryptor: Encryptor, decryptor: Decryptor): Receive = {
|
||||
case chunk: BinaryData =>
|
||||
val decryptor1 = Decryptor.add(decryptor, ByteString.fromArray(chunk))
|
||||
decryptor1.bodies.map(_ => latch.countDown())
|
||||
@ -34,10 +34,12 @@ object EncryptorSpec {
|
||||
context become running(encryptor1, decryptor)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class EncryptorSpec extends FunSuite {
|
||||
|
||||
import EncryptorSpec._
|
||||
|
||||
test("encryption/description tests") {
|
||||
@ -114,4 +116,15 @@ class EncryptorSpec extends FunSuite {
|
||||
latch.await(5, TimeUnit.SECONDS)
|
||||
system.shutdown()
|
||||
}
|
||||
|
||||
test("flare debug unitary") {
|
||||
val key = BinaryData("de0377022997e0b35fdd13e2bb6f2671890719fcb938d4438743d7a8f0c18eaf")
|
||||
val chunk1 = BinaryData("2d837ca635dbb93ad390a679977ecf53598f8cc59c564cba1e9966d5040c51292562b875857e737dca4d26d4cb02c545065f4a6407a7f99e02439e1cd84005729333fbb8744d75931e30f20c8da8a7e1d273d6d82552be1f8be6d7449bd3aacb463ac3093db43c58b2b338d5e3aca6186d3eb362b45d1cac589dc7b61ceae2f741ddc8a279ffe7436076b78126c79da8899399523fda1dc126e5c4f11f6085e586578737952aeee64fa2dc99aad0912813de502a2c6cc7d3b8f3ff375dc7269e425db7414dfa089d533ec349d5a590a8beb1bcc3ecff7c42c446ca240f7ab919f2644adf643958eb658a2b4a16394c5812dee1325b6e3e2cc1e2c38da637893d646c9f830b09bea0fa244c66c1e3dd044ec3feae51803c1836755f4e935267905459f9786c1e4cec3f3988da1fbf2c318ff563b973dc80f3e9402e95c2fa012a83138f4205cc9b7982097a06e82d479f6226ad110a0d92e90aef89be29e0bcadde3b64bf27e6631c4c936d1969934ae66675e39d18ae028c75b47df7fa7399b82775463fce5d70d2c7755c461936c42a27dbfc6fcce517890b5313088b2f88c8b16eecb83c2c5c69c4fd23d64e6b28d6aed1d985bace6ad0f1cb4ebe7d86629565f5c96811d634736f5667886b99a26b531a674607b2ed156768648a5dd8b43df0f585c8636ffca09977be66e1d5f0f969105570e9636412767cae00c96aa79f55d290c5abff234997bd977803817c04e7ca702e618122198ce91ee603cec5ec99237bf3b616c5d8dcd6f7565a63d48acd2723afd3c06ed12b852613e8c61bb08c41d3f50e6a6709b0157120f592425435ed5f59531da48a1a0590ff018e2ff28922a44953997c4c41eb393b98166410a87c5fd5f4f2c8fb54ad69379520917be98af1cfc7239a2ea10f3a22349b1768ff4fde7911786d782dd96bea87e181aa11ca7979118df274aa0e806a0321bee4f300d7e07c3c0a3b70dd221b632e3e0f4887429f03f56f016a87bd09c50844c35c34766e5cc2412ebc80f658101b2779176fd705ad724eaa500afac793866175f34a87fe2c1c8d288d44baa16e41d3fa9c9d9d587f8706682217bce519487a1b4b4aa8f7f3c5649d998d08766b713f13b7f0f2281a64d8c895a8aaa900e581f3415dc063affb31c07055ba1ab003d90dd49dd47f17653c48ecb7683bc89de622485f4b20428a1360916e66d324ee1f59536f8d7f34b002c340c1d56c559aa0094c14d2ea41d266579505b7bac93a76c15a0d558100b9a5ed468c9a89b93527e1457b09141fdfd8f85b49e5a19a6b2b04022d8e4097cdff523b7a47b23cda4e2514ccdccf534b1409a37b4375079dcb0fe467b2d3abb339f7a7c6bcb4f031ea75678a0175a7038bf37fac551cd1e2948777b433cf1e145612a30fccab0ac35ff12bae851ce5a738db2222537697c7fc201c631dd9514a0e7bd79ab93a440b70a27588c03d48a82e4296bb02f150ed616f117d0e641150497897b38cc03877b0cbd9fd5cb6261ce9c9a60e7b6e2929c22bcaf2c56cce484bcc6b5c6943257aa03d9b484bb67590bcb291d69199625e9847617f803bb92d2f780924a6894c4f9ef7939cffed83850456385fdc771e8b9b3a59ddbf914e96756d4d2fac2aea3994f5d7fc18c70fe03085a92bee8f1b7f0903fdd8546cf35ecffb557cfe22e64121ed5dacd82c2ecf52a7bf38d5cc8d1851a5b78b4e5e25230f74f7a17f175dbe72e515956835021bf9819e8033b45b07ef3db5daaeacb851d2c94f571c2093eda094b8d3b90d2af22b56261c5a079d2a56e4ddd138a38cf30af28e11aa5231fcc6e32e032605780ced4b672c1251edc38403ca876e6a830dbd17536ea6e73984761757f2e63fe123c65bd176477d2e3531c0321d6aded27aa2430d17d40669659c041ee956471c68f235fdeff483886364ab93090dd6de22298d73483bf7435398e527e2e250808f0066869f63d907df515d2b5e3fbc5e9057090997bfd49cd560c275a24b689d883c3bd4c2726ed6b3c171c1d83bfda6342339")
|
||||
val chunk2 = BinaryData("400865a51aa22319a563292f2471bde84236d6cb19ece6101a1564ec138f48a327e8163ab188c233b3a9b5513c8f2f5809f3a14fcae1c390c337126edd17a7846aba7964a58ea662ee3a74c2c003f8de40dcfe0e89f7bed904b85e0f64951fdee9a5a7b25404a41f0fb154bd082857bc3dc163b7")
|
||||
val dec = Decryptor(key, 108)
|
||||
val dec12 = Decryptor.add(dec, ByteString.fromArray(BinaryData(chunk1.data ++ chunk2.data)))
|
||||
val dec1 = Decryptor.add(dec, chunk1)
|
||||
val dec2 = Decryptor.add(dec1.copy(bodies = Vector.empty), chunk2)
|
||||
assert(dec12.bodies == dec2.bodies)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,282 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, File}
|
||||
import java.math.BigInteger
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.ask
|
||||
import akka.testkit.TestKit
|
||||
import akka.util.Timeout
|
||||
import com.google.common.io.Files
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.router.FlareRouter._
|
||||
import lightning._
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.implementations.{MultiGraph, MultiNode}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 09/09/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class FlareRouterSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll {
|
||||
|
||||
override def afterAll {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
import FlareRouterSpec._
|
||||
|
||||
/*def edge(bin: BinaryData): NamedEdge = {
|
||||
require(bin.size == 2)
|
||||
NamedEdge(sha256_hash(bin.data(0), bin.data(1), 0, 0))
|
||||
}
|
||||
|
||||
test("add channel") {
|
||||
val myself = BinaryData("01")
|
||||
val graph = new SimpleGraph[bitcoin_pubkey, NamedEdge](classOf[NamedEdge])
|
||||
val channel = channel_desc(sha256_hash(1, 1, 2, 2), myself, BinaryData("02"))
|
||||
val updates = routing_table_update(channel, OPEN) :: Nil
|
||||
val (graph1, updates1) = include(myself, graph, updates, 3, Set())
|
||||
assert(graph1.containsVertex(channel.nodeA))
|
||||
assert(graph1.containsVertex(channel.nodeB))
|
||||
assert(graph1.containsEdge(NamedEdge(channel.channelId)))
|
||||
assert(updates1 == updates)
|
||||
}
|
||||
|
||||
test("ignore channel > radius") {
|
||||
val myself = BinaryData("01")
|
||||
val graph = new SimpleGraph[bitcoin_pubkey, NamedEdge](classOf[NamedEdge])
|
||||
val channel1 = channel_desc(sha256_hash(1, 1, 2, 2), myself, BinaryData("02"))
|
||||
val channel2 = channel_desc(sha256_hash(2, 2, 3, 3), BinaryData("02"), BinaryData("03"))
|
||||
val channel3 = channel_desc(sha256_hash(3, 3, 4, 4), BinaryData("03"), BinaryData("04"))
|
||||
val updates = routing_table_update(channel1, OPEN) :: routing_table_update(channel2, OPEN) :: routing_table_update(channel3, OPEN) :: Nil
|
||||
val (graph1, updates1) = include(myself, graph, updates, 2, Set())
|
||||
assert(graph1.containsVertex(BinaryData("01")))
|
||||
assert(graph1.containsVertex(BinaryData("02")))
|
||||
assert(graph1.containsVertex(BinaryData("03")))
|
||||
assert(!graph1.containsVertex(BinaryData("04")))
|
||||
assert(updates1 == updates.dropRight(1))
|
||||
}
|
||||
|
||||
test("build onion") {
|
||||
val msg = neighbor_onion(Req(beacon_req(BinaryData("01"))))
|
||||
val node3: bitcoin_pubkey = BinaryData("03")
|
||||
val node4: bitcoin_pubkey = BinaryData("04")
|
||||
val onion = buildOnion(node3 :: node4 :: Nil, msg)
|
||||
assert(onion == neighbor_onion(Forward(beacon_forward(BinaryData("03"), neighbor_onion(Forward(beacon_forward(BinaryData("04"), msg)))))))
|
||||
}
|
||||
|
||||
test("graph clone") {
|
||||
val g1 = new SimpleGraph[bitcoin_pubkey, NamedEdge](classOf[NamedEdge])
|
||||
g1.addVertex(BinaryData("01"))
|
||||
g1.addVertex(BinaryData("02"))
|
||||
g1.addVertex(BinaryData("03"))
|
||||
g1.addEdge(BinaryData("01"), BinaryData("02"), edge("0102"))
|
||||
g1.addEdge(BinaryData("02"), BinaryData("03"), edge("0203"))
|
||||
|
||||
val g2 = g1.clone()
|
||||
assert(g1 == g2)
|
||||
}
|
||||
|
||||
test("include updates") {
|
||||
val nodeIds = (0 to 3).map(nodeId).map(bin2pubkey)
|
||||
val myself = nodeIds(0)
|
||||
val graph = new SimpleGraph[bitcoin_pubkey, NamedEdge](classOf[NamedEdge])
|
||||
/*
|
||||
0
|
||||
/ \
|
||||
1 2
|
||||
*/
|
||||
graph.addVertex(nodeIds(0))
|
||||
graph.addVertex(nodeIds(1))
|
||||
graph.addVertex(nodeIds(2))
|
||||
graph.addEdge(nodeIds(0), nodeIds(1), NamedEdge(channelId(nodeIds(0), nodeIds(1))))
|
||||
graph.addEdge(nodeIds(0), nodeIds(2), NamedEdge(channelId(nodeIds(0), nodeIds(2))))
|
||||
val updates = Seq(
|
||||
routing_table_update(channel_desc(channelId(nodeIds(3), nodeIds(1)), nodeIds(3), nodeIds(1)), OPEN),
|
||||
routing_table_update(channel_desc(channelId(nodeIds(1), nodeIds(0)), nodeIds(1), nodeIds(0)), OPEN)
|
||||
)
|
||||
val (graph1, _) = include(myself, graph, updates, 2, Set())
|
||||
assert(graph1.vertexSet().contains(nodeIds(3)))
|
||||
}*/
|
||||
|
||||
/*test("basic routing and discovery") {
|
||||
val radius = 2
|
||||
val maxBeacons = 5
|
||||
val nodeIds = for (i <- 0 to 6) yield nodeId(i)
|
||||
val links = (0, 1) :: (0, 2) :: (1, 3) :: (1, 6) :: (2, 4) :: (4, 5) :: Nil
|
||||
val routers = nodeIds map (nodeId => system.actorOf(Props(new FlareRouter(nodeId, radius, maxBeacons, false)), nodeId.toString().take(6)))
|
||||
|
||||
def createChannel(a: Int, b: Int): Unit = {
|
||||
routers(a) ! genChannelChangedState(routers(b), nodeIds(b), FlareRouterSpec.channelId(nodeIds(a), nodeIds(b)), 1000000000)
|
||||
routers(b) ! genChannelChangedState(routers(a), nodeIds(a), FlareRouterSpec.channelId(nodeIds(a), nodeIds(b)), 0)
|
||||
}
|
||||
|
||||
links.foreach { case (a, b) => createChannel(a, b) }
|
||||
|
||||
Thread.sleep(5000)
|
||||
|
||||
implicit val timeout = Timeout(3 seconds)
|
||||
import system.dispatcher
|
||||
|
||||
val futures = for (i <- 0 until nodeIds.length) yield (routers(i) ? 'info).mapTo[FlareInfo].map(info => i -> info)
|
||||
val future = Future.sequence(futures).map(_.toMap)
|
||||
val infos = Await.result(future, 3 seconds)
|
||||
assert(infos(0).neighbors === 2)
|
||||
}
|
||||
|
||||
test("find remote beacons") {
|
||||
val length = 30
|
||||
val radius = 2
|
||||
val maxBeacons = 1
|
||||
val nodeIds = (0 to length).map(nodeId).map(bin2pubkey)
|
||||
val closestToFirst = nodeIds.sortBy(id => distance(nodeIds(0), id))
|
||||
val order = (closestToFirst.drop(1) :+ nodeIds(0)).reverse
|
||||
val links = order.sliding(2)
|
||||
// so we have A -> B -> C -> D -> .... -> Z with d(A,B) > d(A,C) > ... > d(A, Z)
|
||||
// in other words A's best beacon is Z
|
||||
val routers: Map[bitcoin_pubkey, ActorRef] = order.map(nodeId => (nodeId, system.actorOf(Props(new FlareRouter(nodeId, radius, maxBeacons, false))))).toMap
|
||||
def createChannel(a: bitcoin_pubkey, b: bitcoin_pubkey): Unit = {
|
||||
routers(a) ! genChannelChangedState(routers(b), b, FlareRouterSpec.channelId(a, b), 1000000000)
|
||||
routers(b) ! genChannelChangedState(routers(a), a, FlareRouterSpec.channelId(a, b), 0)
|
||||
}
|
||||
for (link <- links) createChannel(link.head, link.last)
|
||||
implicit val timeout = Timeout(1 second)
|
||||
awaitCond(Await.result(routers(order.head) ? 'beacons, 1 second).asInstanceOf[Set[Beacon]].map(_.id).contains(order.last), 10 seconds)
|
||||
}*/
|
||||
|
||||
test("find directed route") {
|
||||
/*
|
||||
(10) (2) (5)
|
||||
A ---1--> B ---2--> C ---3--> D
|
||||
\ 6(5) ^
|
||||
\ (2) v (7) |
|
||||
4----> E ----5---/
|
||||
*/
|
||||
def channelId(i: Int) = sha256_hash(i, i, i, i)
|
||||
def nodeId(c: String) = bitcoin_pubkey(BinaryData(c * 33)) // "aa" => "aaaaaaaaaaa.....aaaaa"
|
||||
val states = channel_state_update(channelId(1), 0, nodeId("aa"), 10, 0) ::
|
||||
channel_state_update(channelId(1), 0, nodeId("bb"), 1, 0) ::
|
||||
channel_state_update(channelId(2), 0, nodeId("bb"), 2, 0) ::
|
||||
channel_state_update(channelId(2), 0, nodeId("cc"), 30, 0) ::
|
||||
channel_state_update(channelId(3), 0, nodeId("cc"), 5, 0) ::
|
||||
channel_state_update(channelId(3), 0, nodeId("dd"), 10, 0) ::
|
||||
channel_state_update(channelId(4), 0, nodeId("aa"), 2, 0) ::
|
||||
channel_state_update(channelId(4), 0, nodeId("ee"), 4, 0) ::
|
||||
channel_state_update(channelId(5), 0, nodeId("ee"), 7, 0) ::
|
||||
channel_state_update(channelId(5), 0, nodeId("cc"), 10, 0) ::
|
||||
channel_state_update(channelId(6), 0, nodeId("bb"), 5, 0) ::
|
||||
channel_state_update(channelId(6), 0, nodeId("ee"), 10, 0) :: Nil
|
||||
val path = findPaymentRoute(nodeId("aa"), nodeId("dd"), 4, states)
|
||||
assert(path === nodeId("aa") :: nodeId("bb") :: nodeId("ee") :: nodeId("cc") :: nodeId("dd") :: Nil)
|
||||
}
|
||||
|
||||
ignore("display graph") {
|
||||
val g = new MultiGraph("test")
|
||||
val n01 = g.addNode[MultiNode]("01")
|
||||
val n02 = g.addNode[MultiNode]("02")
|
||||
val n03 = g.addNode[MultiNode]("03")
|
||||
val n04 = g.addNode[MultiNode]("04")
|
||||
val n05 = g.addNode[MultiNode]("05")
|
||||
val n06 = g.addNode[MultiNode]("06")
|
||||
val n07 = g.addNode[MultiNode]("07")
|
||||
val n08 = g.addNode[MultiNode]("08")
|
||||
val n09 = g.addNode[MultiNode]("09")
|
||||
def channelId(i: Int) = sha2562string(sha256_hash(i, i, i, i))
|
||||
g.addEdge(channelId(1), n01, n02, true)
|
||||
g.addEdge(channelId(2), n02, n03, true)
|
||||
g.addEdge(channelId(3), n03, n04, true)
|
||||
g.addEdge(channelId(4), n02, n05, true)
|
||||
g.addEdge(channelId(5), n05, n06, true)
|
||||
g.addEdge(channelId(6), n06, n07, true)
|
||||
g.addEdge(channelId(7), n01, n08, true)
|
||||
g.addEdge(channelId(8), n08, n09, true)
|
||||
val beacons: Set[Beacon] = Set(Beacon(bitcoin_pubkey(BinaryData("04")), null, 99), Beacon(bitcoin_pubkey(BinaryData("07")), null, 99))
|
||||
val dynamic: Map[ChannelOneEnd, channel_state_update] = Map(
|
||||
ChannelOneEnd(BinaryData(channelId(1)), BinaryData("01")) -> channel_state_update(BinaryData(channelId(1)), 0, BinaryData("01"), 90, 0),
|
||||
ChannelOneEnd(BinaryData(channelId(1)), BinaryData("02")) -> channel_state_update(BinaryData(channelId(1)), 0, BinaryData("02"), 10, 0)
|
||||
)
|
||||
|
||||
val nodes = g.getNodeIterator[MultiNode].map(node => s""""${node.getId.take(6)}" [tooltip="${node.getId}"];""").mkString("\n")
|
||||
val centerColor = s""""${g.getNode[MultiNode](0).getId.take(6)}" [color=blue];"""
|
||||
val beaconsColor = beacons.map(beacon => s""""${g.getNode[MultiNode](pubkey2string(beacon.id)).getId.take(6)}" [color=red];""").mkString("\n")
|
||||
val edges = g.getEdgeSet[Edge]
|
||||
.flatMap(edge => {
|
||||
val src = ChannelOneEnd(BinaryData(edge.getId), BinaryData(edge.getSourceNode[MultiNode].getId))
|
||||
val srcAvail = dynamic.get(src).map(_.amountMsat)
|
||||
val tgt = ChannelOneEnd(BinaryData(edge.getId), BinaryData(edge.getTargetNode[MultiNode].getId))
|
||||
val tgtAvail = dynamic.get(tgt).map(_.amountMsat)
|
||||
s""" "${pubkey2string(src.node_id)}" -> "${pubkey2string(tgt.node_id)}" [fontsize=8 labeltooltip="${edge.getId}" label=${srcAvail.getOrElse("unknown")}];""" ::
|
||||
s""" "${pubkey2string(tgt.node_id)}" -> "${pubkey2string(src.node_id)}" [fontsize=8 labeltooltip="${edge.getId}" label=${tgtAvail.getOrElse("unknown")}];""" :: Nil
|
||||
}).mkString("\n")
|
||||
|
||||
val dot =
|
||||
s"""digraph G {
|
||||
|rankdir=LR;
|
||||
|$nodes
|
||||
|$centerColor
|
||||
|$beaconsColor
|
||||
|$edges
|
||||
|}
|
||||
""".stripMargin
|
||||
|
||||
println(dot)
|
||||
|
||||
{
|
||||
import scala.sys.process._
|
||||
val input = new ByteArrayInputStream(dot.getBytes)
|
||||
val output = new ByteArrayOutputStream()
|
||||
"dot -Tpng" #< input #> output !
|
||||
|
||||
//Files.write(img, new File("g.png"))
|
||||
}
|
||||
|
||||
/*digraph G {
|
||||
rankdir=LR;
|
||||
"a" [color=blue];
|
||||
"a" [tooltip="titi"];
|
||||
"d" [color=red];
|
||||
"g" [color=red];
|
||||
"a" -> "b" [label = "90" ];
|
||||
"b" -> "a" [fontsize=8 label = "10" labeltooltip="toto"];
|
||||
"b" -> "c"
|
||||
"c" -> "d"
|
||||
"b" -> "e"
|
||||
"e" -> "f"
|
||||
"f" -> "g"
|
||||
"a" -> "h"
|
||||
"h" -> "i"
|
||||
}*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object FlareRouterSpec {
|
||||
val random = new Random()
|
||||
|
||||
def channelId(a: BinaryData, b: BinaryData): BinaryData = {
|
||||
if (Scripts.isLess(a, b)) Crypto.sha256(a ++ b) else Crypto.sha256(b ++ a)
|
||||
}
|
||||
|
||||
def nodeId(i: Int): BinaryData = {
|
||||
val a = BigInteger.valueOf(i).toByteArray
|
||||
Crypto.sha256(a)
|
||||
}
|
||||
|
||||
def genChannelChangedState(them: ActorRef, theirNodeId: BinaryData, channelId: BinaryData, available_amount: Int): ChannelChangedState =
|
||||
ChannelChangedState(null, them, theirNodeId, null, NORMAL, DATA_NORMAL(new Commitments(null, null,
|
||||
OurCommit(0, CommitmentSpec(Set(), 0, 0, 0, available_amount, 0), null),
|
||||
null, null, null, 0L, null, null, null, null) {
|
||||
override def anchorId: BinaryData = channelId // that's the only thing we need
|
||||
}, null, null))
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair._
|
||||
import org.graphstream.algorithm.Dijkstra
|
||||
import org.graphstream.graph._
|
||||
import org.graphstream.graph.implementations._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuiteLike
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
/**
|
||||
* Created by PM on 26/09/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class GraphStreamSpec extends FunSuiteLike {
|
||||
|
||||
test("remove node") {
|
||||
val g = new MultiGraph("test")
|
||||
val a = g.addNode[MultiNode]("a")
|
||||
val b = g.addNode[MultiNode]("b")
|
||||
val c = g.addNode[MultiNode]("c")
|
||||
g.addEdge("a-b", a, b)
|
||||
g.addEdge("b-c", b, c)
|
||||
assert(g.getNodeCount === 3)
|
||||
assert(g.getEdgeCount === 2)
|
||||
g.removeNode("b")
|
||||
assert(g.getNodeCount === 2)
|
||||
assert(g.getEdgeCount === 0)
|
||||
}
|
||||
|
||||
test("get nonexisting node") {
|
||||
val g = new MultiGraph("test")
|
||||
assert(Option(g.getNode("a")).isEmpty)
|
||||
}
|
||||
|
||||
test("get nonexisting edge") {
|
||||
val g = new MultiGraph("test")
|
||||
assert(Option(g.getEdge("a-b")).isEmpty)
|
||||
}
|
||||
|
||||
test("remove nonexisting edge") {
|
||||
val g = new MultiGraph("test")
|
||||
intercept[ElementNotFoundException] {
|
||||
g.removeEdge("a-b")
|
||||
}
|
||||
}
|
||||
|
||||
test("duplicate node") {
|
||||
val g = new MultiGraph("test")
|
||||
val a = g.addNode[MultiNode]("a")
|
||||
val b = g.addNode[MultiNode]("b")
|
||||
g.addEdge("a-b", a, b)
|
||||
intercept[IdAlreadyInUseException] {
|
||||
g.addNode[MultiNode]("a")
|
||||
}
|
||||
}
|
||||
|
||||
test("simple dijsktra") {
|
||||
val g = new MultiGraph("test")
|
||||
val a = g.addNode[MultiNode]("a")
|
||||
val b = g.addNode[MultiNode]("b")
|
||||
val c = g.addNode[MultiNode]("c")
|
||||
val ab = g.addEdge[Edge]("a-b", a, b)
|
||||
val bc = g.addEdge[Edge]("b-c", b, c)
|
||||
val dijkstra = new Dijkstra()
|
||||
dijkstra.init(g)
|
||||
dijkstra.setSource(a)
|
||||
dijkstra.compute()
|
||||
assert(dijkstra.getPathNodes[MultiNode](c).toList.reverse === a :: b :: c :: Nil)
|
||||
assert(dijkstra.getPathEdges[Edge](c).toList.reverse === ab :: bc :: Nil)
|
||||
}
|
||||
|
||||
test("simple dijsktra after graph copy") {
|
||||
val g = new MultiGraph("test")
|
||||
val a = g.addNode[MultiNode]("a")
|
||||
val b = g.addNode[MultiNode]("b")
|
||||
val c = g.addNode[MultiNode]("c")
|
||||
val ab = g.addEdge[Edge]("a-b", a, b)
|
||||
val bc = g.addEdge[Edge]("b-c", b, c)
|
||||
|
||||
val dijkstra = new Dijkstra()
|
||||
dijkstra.init(g)
|
||||
dijkstra.setSource(a)
|
||||
dijkstra.compute()
|
||||
assert(dijkstra.getPathNodes[MultiNode](c).toList.reverse === a :: b :: c :: Nil)
|
||||
assert(dijkstra.getPathEdges[Edge](c).toList.reverse === ab :: bc :: Nil)
|
||||
|
||||
val g1 = FlareRouter.copy(g)
|
||||
val a1 = g1.getNode[MultiNode]("a")
|
||||
val b1 = g1.getNode[MultiNode]("b")
|
||||
val c1 = g1.getNode[MultiNode]("c")
|
||||
val ab1 = g1.getEdge[Edge]("a-b")
|
||||
val bc1 = g1.getEdge[Edge]("b-c")
|
||||
|
||||
val dijkstra1 = new Dijkstra()
|
||||
dijkstra1.init(g1)
|
||||
dijkstra1.setSource(a1)
|
||||
dijkstra1.compute()
|
||||
assert(dijkstra1.getPathNodes[MultiNode](c1).toList.reverse === a1 :: b1 :: c1 :: Nil)
|
||||
assert(dijkstra1.getPathEdges[Edge](c1).toList.reverse === ab1 :: bc1 :: Nil)
|
||||
}
|
||||
|
||||
test("dijkstra to unreachable") {
|
||||
val g = new MultiGraph("test")
|
||||
val a = g.addNode[MultiNode]("a")
|
||||
val b = g.addNode[MultiNode]("b")
|
||||
val c = g.addNode[MultiNode]("c")
|
||||
g.addEdge("a-b", a, b)
|
||||
|
||||
val dijkstra = new Dijkstra()
|
||||
dijkstra.init(g)
|
||||
dijkstra.setSource(a)
|
||||
dijkstra.compute()
|
||||
assert(dijkstra.getPathLength(c) === Double.PositiveInfinity)
|
||||
}
|
||||
|
||||
test("node factory") {
|
||||
val g = new MultiGraph("test")
|
||||
case class MyNode(_graph: AbstractGraph, _id: String) extends MultiNode(_graph, _id) {
|
||||
val pubkey = bin2pubkey(BinaryData(_id))
|
||||
}
|
||||
val nodeFactory = new NodeFactory[MyNode] {
|
||||
override def newInstance(id: String, graph: Graph): MyNode = MyNode(graph.asInstanceOf[AbstractGraph], id)
|
||||
}
|
||||
g.setNodeFactory(nodeFactory)
|
||||
val node = g.addNode[MyNode]("010203")
|
||||
assert(node === MyNode(g, "010203"))
|
||||
}
|
||||
|
||||
test("edge factory") {
|
||||
val g = new MultiGraph("test")
|
||||
case class MyEdge(_id: String, _source: Node, _target: Node, _directed: Boolean) extends AbstractEdge(_id, _source.asInstanceOf[AbstractNode], _target.asInstanceOf[AbstractNode], _directed) {
|
||||
//val pubkey = bin2pubkey(BinaryData(_id))
|
||||
}
|
||||
val edgeFactory = new EdgeFactory[MyEdge] {
|
||||
override def newInstance(id: String, src: Node, dst: Node, directed: Boolean): MyEdge = MyEdge(id, src, dst, directed)
|
||||
}
|
||||
g.setEdgeFactory(edgeFactory)
|
||||
val a = g.addNode[Node]("a")
|
||||
val b = g.addNode[Node]("b")
|
||||
val edge = g.addEdge[MyEdge]("a-b", a, b)
|
||||
assert(edge === MyEdge("a-b", a, b, false))
|
||||
}
|
||||
|
||||
test("clone graph") {
|
||||
val g1 = new MultiGraph("test")
|
||||
val a = g1.addNode[MultiNode]("a")
|
||||
val b = g1.addNode[MultiNode]("b")
|
||||
val c = g1.addNode[MultiNode]("c")
|
||||
g1.addEdge("a-b", a, b)
|
||||
g1.addEdge("b-c", b, c)
|
||||
val g2 = FlareRouter.copy(g1)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
package fr.acinq.eclair
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
|
||||
import akka.actor.{ActorSystem, Props, Status}
|
||||
import akka.testkit.{TestFSMRef, TestKit, TestProbe}
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.router._
|
||||
import lightning.sha256_hash
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
/**
|
||||
* Created by PM on 29/08/2016.
|
||||
@ -21,7 +21,7 @@ class PaymentFSMSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
test("route not available") {
|
||||
/*test("route not available") {
|
||||
val router = system.actorOf(Props[Router])
|
||||
val selector = system.actorOf(Props[ChannelSelector])
|
||||
val channel00 = TestProbe()
|
||||
@ -116,6 +116,6 @@ class PaymentFSMSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with
|
||||
channel01.expectMsgType[CMD_ADD_HTLC]
|
||||
sender.send(paymentFsm, PaymentFailed(channel01.ref, req.h, "some reason"))
|
||||
sender.expectMsgType[Status.Failure]
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package fr.acinq.eclair
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.router.{ChannelDesc, PaymentManager, Router}
|
||||
import fr.acinq.eclair.{Globals, _}
|
||||
import lightning.route_step
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
@ -13,7 +13,7 @@ import org.scalatest.junit.JUnitRunner
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class RouterSpec extends FunSuite {
|
||||
|
||||
test("calculate simple route") {
|
||||
/*test("calculate simple route") {
|
||||
|
||||
val channels: Map[BinaryData, ChannelDesc] = Map(
|
||||
BinaryData("0a") -> ChannelDesc(BinaryData("0a"), BinaryData("01"), BinaryData("02")),
|
||||
@ -103,6 +103,6 @@ class RouterSpec extends FunSuite {
|
||||
assert(route.steps(1).amount == amountMsat)
|
||||
assert(route.steps.dropRight(1).map(_.next.bitcoin.get.key).map(bytestring2bin) == nodeIds)
|
||||
assert(route.steps(0).amount - route.steps(1).amount == nodeFee(Globals.base_fee, Globals.proportional_fee, route.steps(1).amount))
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>fr.acinq.eclair</groupId>
|
||||
<artifactId>eclair_2.11</artifactId>
|
||||
<version>0.2-SNAPSHOT</version>
|
||||
<version>0.2-flare-dynamic-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>lightning-types_2.11</artifactId>
|
||||
|
||||
@ -19,7 +19,7 @@ message sha256_hash {
|
||||
}
|
||||
|
||||
message rval {
|
||||
option (scalapb.message).extends = "lightning.RvalToString";
|
||||
option (scalapb.message).extends = "lightning.RvalToString";
|
||||
required fixed64 a = 1;
|
||||
required fixed64 b = 2;
|
||||
required fixed64 c = 3;
|
||||
@ -221,6 +221,108 @@ message error {
|
||||
optional string problem = 1;
|
||||
}
|
||||
|
||||
message routing_table {
|
||||
repeated channel_open channels = 1;
|
||||
}
|
||||
|
||||
message neighbor_hello {
|
||||
required routing_table routing_table = 1;
|
||||
}
|
||||
|
||||
message channel_open {
|
||||
required sha256_hash channel_id = 1;
|
||||
required bitcoin_pubkey node_a = 2;
|
||||
required bitcoin_pubkey node_b = 3;
|
||||
}
|
||||
|
||||
message channel_close {
|
||||
required sha256_hash channel_id = 1;
|
||||
}
|
||||
|
||||
message routing_table_update {
|
||||
oneof update {
|
||||
channel_open channel_open = 1;
|
||||
channel_close channel_close = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message channel_state_update {
|
||||
required sha256_hash channel_id = 1;
|
||||
// updates are ordered
|
||||
required uint32 sequence = 2;
|
||||
required bitcoin_pubkey node = 3;
|
||||
// available amount for payment *from* this node through this channel
|
||||
required uint32 amount_msat = 4;
|
||||
// fee requested to relay payment *to* this channel
|
||||
required uint32 relay_fee = 5;
|
||||
}
|
||||
|
||||
message neighbor_update {
|
||||
repeated routing_table_update routing_table_updates = 1;
|
||||
repeated channel_state_update channel_state_updates = 2;
|
||||
}
|
||||
|
||||
message neighbor_reset {
|
||||
// here are my channel ids, please send me the diff
|
||||
repeated sha256_hash channels_ids = 1;
|
||||
}
|
||||
|
||||
message forward {
|
||||
required bitcoin_pubkey to_node = 1;
|
||||
required neighbor_onion onion = 2;
|
||||
}
|
||||
|
||||
message beacon_req {
|
||||
required bitcoin_pubkey origin = 1;
|
||||
// so that they can find a route back to me
|
||||
repeated channel_open channels = 3;
|
||||
}
|
||||
|
||||
message beacon_ack {
|
||||
required bitcoin_pubkey origin = 1;
|
||||
// optional better candidate
|
||||
optional bitcoin_pubkey alternative = 2;
|
||||
// so that they can find a route to the candidate
|
||||
repeated channel_open channels = 3;
|
||||
}
|
||||
|
||||
message subscribe {
|
||||
required bitcoin_pubkey origin = 1;
|
||||
// so that they can find a route back to me
|
||||
repeated channel_open channels = 3;
|
||||
}
|
||||
|
||||
message unsubscribe {
|
||||
required bitcoin_pubkey origin = 1;
|
||||
}
|
||||
|
||||
message channel_states_request {
|
||||
required bitcoin_pubkey origin = 1;
|
||||
required sha256_hash request_id = 2;
|
||||
// so that they can find a route back to me
|
||||
repeated channel_open channels = 3;
|
||||
}
|
||||
|
||||
message channel_states_response {
|
||||
required bitcoin_pubkey origin = 1;
|
||||
required sha256_hash request_id = 2;
|
||||
repeated channel_state_update states = 3;
|
||||
}
|
||||
|
||||
message neighbor_onion {
|
||||
// Where to next?
|
||||
oneof next {
|
||||
forward forward = 1;
|
||||
beacon_req req = 10;
|
||||
beacon_ack ack = 11;
|
||||
subscribe subscribe = 20;
|
||||
unsubscribe unsubscribe = 21;
|
||||
channel_state_update dynamic_info = 22;
|
||||
channel_states_request channel_states_request = 30;
|
||||
channel_states_response channel_states_response = 31;
|
||||
}
|
||||
}
|
||||
|
||||
// This is the union which defines all of them
|
||||
message pkt {
|
||||
oneof pkt {
|
||||
@ -247,5 +349,20 @@ message pkt {
|
||||
|
||||
// Unexpected issue.
|
||||
error error = 40;
|
||||
|
||||
// Gossip
|
||||
neighbor_hello neighbor_hello = 60;
|
||||
neighbor_update neighbor_update = 61;
|
||||
neighbor_reset neighbor_reset = 62;
|
||||
neighbor_onion neighbor_onion = 63;
|
||||
}
|
||||
}
|
||||
|
||||
message payment_request {
|
||||
required bitcoin_pubkey node_id = 1;
|
||||
required uint32 amount_msat = 2;
|
||||
required sha256_hash r_hash = 3;
|
||||
required routing_table routing_table = 4;
|
||||
repeated channel_state_update states = 5;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user