Compare commits

...

105 Commits

Author SHA1 Message Date
pm47
8a85afb5b9 added route iterations support to Simulator 2016-10-05 20:46:57 +02:00
sstone
d2d2d673cb ignore test that requires dot on the path 2016-10-05 18:03:03 +02:00
sstone
1cb24d3082 travis quickfix 2016-10-05 17:52:09 +02:00
sstone
e21806e862 fix typo in logback.xml 2016-10-05 17:51:54 +02:00
pm47
deea68fac1 fixed simulator 2016-10-05 17:38:14 +02:00
sstone
f7a5852e0f Merge branch 'master' into wip-flare 2016-10-05 16:46:21 +02:00
sstone
c9bf29a957 make simulator use actual Channels 2016-10-05 16:32:14 +02:00
pm47
fd7c109eb1 added alternate route prototype 2016-10-05 12:18:10 +02:00
pm47
a96fdec7f3 got rid of buggy NetMetricsActor 2016-10-04 18:31:52 +02:00
pm47
ca7922971c dynamic info prototype 2016-10-04 18:24:03 +02:00
pm47
9bba401543 added a channel_info protobuf message and changed version to flare-dynamic 2016-09-27 15:38:36 +02:00
pm47
457841120a fixed typo in simulator metrics 2016-09-27 13:57:10 +02:00
pm47
b5ae68703f changed addEdge calls 2016-09-26 19:40:40 +02:00
pm47
e40290875f changed version to 0.2-flare-SNAPSHOT 2016-09-26 19:30:06 +02:00
pm47
7e055564df replaced jgrapht by graph-stream 2016-09-26 19:29:08 +02:00
pm47
6ca4957f3e fixed simulator logging 2016-09-26 11:18:14 +02:00
pm47
1647054ac2 added more stats to simulator 2016-09-23 18:07:03 +02:00
pm47
96576ca662 fixed YA embarassing bug in net metrics actor 2016-09-23 17:20:45 +02:00
pm47
8ca1fd5441 fixed typo 2016-09-23 15:06:55 +02:00
pm47
96572c884c fixed embarassing bug in net metrics actor 2016-09-23 15:06:47 +02:00
pm47
ad0b3beed3 added a configurable beaconReactivateCount 2016-09-23 14:49:35 +02:00
pm47
df0f0df152 added a NetMetricsActor 2016-09-23 14:13:46 +02:00
pm47
5723cee340 moved Simulator to main dir and created a dedicated jar 2016-09-22 17:12:22 +02:00
pm47
b7b84f301f merged 2016-09-22 15:34:07 +02:00
pm47
a20b7ba456 better progress display 2016-09-22 15:33:16 +02:00
pm47
b843ea4ae3 added menu every 30 seconds 2016-09-22 15:20:33 +02:00
sstone
cd1ea4f1a4 simulator: add option to save graph data 2016-09-22 15:03:34 +02:00
pm47
1fffdfd873 changed Simulator parameters 2016-09-22 14:50:21 +02:00
pm47
87568fbb55 fixed build 2016-09-22 14:37:36 +02:00
pm47
d254c035f5 removed reflection in actor instantiation 2016-09-22 14:15:52 +02:00
pm47
84b8279987 Merge branch 'wip-flare-fd' of github.com:ACINQ/eclair into wip-flare-fd 2016-09-22 14:07:42 +02:00
pm47
120fbebae8 made ticks optional in FlareRouter 2016-09-22 14:07:37 +02:00
sstone
990d0fc8f4 minor cleanup 2016-09-22 14:06:11 +02:00
sstone
f531127988 Merge branch 'wip-flare-fd' of https://github.com/ACINQ/eclair into wip-flare-fd 2016-09-22 13:56:36 +02:00
sstone
a4826918b0 add simple grapĥ generator and use it in our simulator 2016-09-22 13:56:14 +02:00
pm47
08a2039ac5 added remote beacon test 2016-09-22 13:56:08 +02:00
pm47
e31fa6fb0c got rid of actorselection 2016-09-22 13:04:42 +02:00
pm47
affcff3673 Merge branch 'wip-flare-fd' of github.com:ACINQ/eclair into wip-flare-fd 2016-09-22 11:55:36 +02:00
pm47
d5a6f02368 added auth handler reference to channel changestate event 2016-09-22 11:55:30 +02:00
sstone
d6733975aa fix router tests 2016-09-21 18:55:59 +02:00
sstone
b6218da288 Merge branch 'wip-flare-fd' of /home/fabrice/code/eclair-private into wip-flare-fd 2016-09-21 18:33:43 +02:00
sstone
6be815afe5 add simulator 2016-09-21 17:45:19 +02:00
sstone
5ea523a08e make htlc scripts more readable 2016-09-20 15:40:15 +02:00
pm47
bade5ddf0d properly handle max graph size 2016-09-19 17:55:39 +02:00
pm47
1a19f58771 typed graph vertex to bitcoin_pubkey 2016-09-19 16:34:42 +02:00
pm47
a73c3f2b29 added dot export to gui 2016-09-19 15:31:09 +02:00
pm47
72946faa62 renamed router actor 2016-09-19 15:30:41 +02:00
pm47
9b97bb664c make TCP/HTTP ports more visible in the gui 2016-09-19 14:25:59 +02:00
pm47
2fc52ffbd8 added neighbor_reset 2016-09-19 13:49:33 +02:00
pm47
78d2f8d961 added a require to route size 2016-09-19 11:38:27 +02:00
pm47
f234b1fea2 disabled compat test 2016-09-18 14:45:18 +02:00
pm47
cde9181676 grouped case classes 2016-09-18 14:24:27 +02:00
pm47
1adf972caf removed unused dependencies 2016-09-18 14:18:14 +02:00
pm47
1b0c7ecee3 fixed typo 2016-09-18 14:18:06 +02:00
pm47
1cd5119ae0 removed papertrail appender 2016-09-18 14:10:46 +02:00
pm47
47253233c6 added serializers 2016-09-17 21:47:07 +02:00
pm47
0544b804e9 removed hardcoded radius 2016-09-17 21:27:22 +02:00
pm47
d2b40c61bd hardcoded radius=3 2016-09-17 20:19:15 +02:00
pm47
d82a4d7940 added flare_info api method and now send beacon_req to newly discovered nodes after merge 2016-09-17 18:28:11 +02:00
pm47
71eba309af hardcoded radius=2 2016-09-17 17:44:03 +02:00
pm47
7c857c9be5 incresed update tick timeout 2016-09-17 17:42:15 +02:00
pm47
a34538de3a we are now only sending updates once to a given neighbor 2016-09-17 15:07:45 +02:00
pm47
ac51e64247 disabled ticks 2016-09-17 01:27:52 +02:00
pm47
d24956bdac disabled neighbor_reset 2016-09-17 00:12:26 +02:00
pm47
d82062898f send neighbor_reset to 3 neighbors 2016-09-16 18:07:41 +02:00
pm47
92ae29fdef send reset to all neighbors 2016-09-16 17:26:45 +02:00
pm47
35da8d7dad now using neighbor_reset messages 2016-09-16 16:38:25 +02:00
pm47
bb07d794a1 only send an extract of the table to neighbors 2016-09-16 15:30:17 +02:00
pm47
4bea3741ed periodically send table to neighbors 2016-09-16 14:53:17 +02:00
pm47
fbdfed4ec7 refactored send (not a fatal error to not find neighbor, can happen under certain race conditions) 2016-09-16 14:09:19 +02:00
pm47
2bd0c52f38 fixed graph inversion 2016-09-16 10:56:35 +02:00
pm47
10aaedb870 Merge branch 'wip-flare' of github.com:ACINQ/eclair-private into wip-flare 2016-09-16 09:29:31 +02:00
sstone
98fdc60823 bugfix: incorrect handling of truncated messages 2016-09-16 09:27:17 +02:00
pm47
5ce8d8494e changed beacon detection routine 2016-09-16 09:25:05 +02:00
pm47
493d826881 added debug logs 2016-09-15 20:51:17 +02:00
pm47
4f92e34c26 removed unused code 2016-09-15 20:16:47 +02:00
pm47
51a6a84503 fixed YA bug related to graph beeing immutable 2016-09-15 18:49:34 +02:00
pm47
ac5a979517 removed beacon_set 2016-09-15 18:17:39 +02:00
pm47
c0186bcd94 temporary disabled SecureRandom (linux bug) 2016-09-15 16:29:31 +02:00
pm47
8a583cd813 including route to us when forwarding 2016-09-15 15:36:51 +02:00
pm47
b0dbe47d49 improved logging 2016-09-15 15:04:52 +02:00
pm47
779a366d7c now returning full route when referring a beacon 2016-09-15 14:49:33 +02:00
pm47
94cf8fcff9 improved beacon management 2016-09-15 13:32:50 +02:00
pm47
2232239f9e always send route with req msg 2016-09-15 11:10:57 +02:00
pm47
e601912df6 improved logging 2016-09-15 10:26:54 +02:00
pm47
d15ccec680 fixed issue with alternate routes not stored 2016-09-14 20:15:12 +02:00
pm47
5ec9ea2c23 changed router log level 2016-09-14 18:50:42 +02:00
pm47
2689110e30 simplified FlareRouter.include 2016-09-14 18:26:29 +02:00
pm47
81e8211a45 attempted to fix flare beacon list 2016-09-14 16:52:37 +02:00
sstone
c99a2eb7cb Merge branch 'wip-flare' of https://github.com/ACINQ/eclair-private into wip-flare 2016-09-14 15:17:03 +02:00
sstone
ee08128fa8 api: add findroute method
input: a PaymentRequest (protobuf message encoded as a Base 64 string)
example:
{
    "id": 12345,
    "method": "findroute",
    "params" : [ "CiMKIQL97RvvP9y843JPlmPW3dn5bX2juG98y+mtOep14jwxSxCAvYMUGiQJlAAqvtCFccAR/2nTh3Rv/BkZQEvrId62TfIhAQhKJp6lsA0icgpwCiQJIWcO8lVaGnMRBF3keCDLqiwZyB2Nbn3g6HQhtXXXemow1FoSIwohAv3tG+8/3Lzjck+WY9bd2fltfaO4b3zL6a056nXiPDFLGiMKIQO++0+K0dh9TEGsuzFnkf4VfzBcryEjyEj0SJdar4XBuw==" ]
}

output: a route represented as a list of node ids
example:
{
  "result": {
    "route": [
      "03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb",
      "02fded1bef3fdcbce3724f9663d6ddd9f96d7da3b86f7ccbe9ad39ea75e23c314b"
    ]
  },
  "id": "12345"
}
2016-09-14 15:11:55 +02:00
pm47
02830850a1 now using a random seed 2016-09-14 13:35:13 +02:00
pm47
23dc2a32ef increased block interval to 3s 2016-09-14 13:12:08 +02:00
sstone
d2c340030f Merge branch 'wip-flare' of https://github.com/ACINQ/eclair-private into wip-flare 2016-09-14 13:03:24 +02:00
sstone
0d642bf52f use payment request to send and receive payments
add a payment request protobuf message, and use it (encoded base64) to send
and receive payments from our GUI and API
2016-09-14 13:00:11 +02:00
pm47
bc9d727fd2 now using a *fake* bitcoin client (and cleaned up real bitcoin client) 2016-09-14 12:48:43 +02:00
pm47
9e96fb94fa added a 'get beacons' api method 2016-09-14 12:03:38 +02:00
pm47
d715e7eaa8 added router back to Service 2016-09-14 11:58:37 +02:00
pm47
d91fc66b8c wip payment 2016-09-14 11:38:36 +02:00
pm47
419c747bb9 implemented flare neighbor handler (payment not ready) 2016-09-13 18:17:29 +02:00
pm47
8b4d38756c set version to 0.2-flare-pm-SNAPSHOT 2016-09-12 17:25:59 +02:00
pm47
76406145fa added random graph generation 2016-09-12 17:23:42 +02:00
pm47
5245cbc006 improved logging 2016-09-12 17:23:19 +02:00
pm47
7f7b9e5711 basic beacon handling simulator 2016-09-12 16:35:55 +02:00
PM
e847ed5f4e basic neighbor handling simulator 2016-09-09 14:57:47 +02:00
48 changed files with 1959 additions and 501 deletions

2
.gitignore vendored
View File

@ -27,3 +27,5 @@ DeleteMe*.*
*~
result.txt
*.gv
*.dot

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 _ => {}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 _ => {}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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] = ???
}

View File

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

View 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()
}
}
}

View File

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

View File

@ -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] = ???

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]
}
}*/
}

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-SNAPSHOT</version>
<version>0.2-flare-dynamic-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>