Eclair performance tests

This commit is contained in:
rorp 2020-01-07 10:02:08 -08:00
parent d776e1c952
commit e1acac05eb
15 changed files with 679 additions and 105 deletions

View file

@ -0,0 +1,142 @@
package org.bitcoins.bench.eclair
import java.io.File
import java.nio.file.{Files, StandardOpenOption}
import akka.actor.ActorSystem
import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.eclair.rpc.api.PaymentId
import org.bitcoins.testkit.async.TestAsyncUtil
import org.bitcoins.testkit.eclair.rpc.EclairRpcTestUtil
import scala.collection.JavaConverters._
import scala.concurrent.duration.DurationInt
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
object EclairBench extends App with EclairRpcTestUtil {
import PaymentLog._
implicit val system = ActorSystem()
import system.dispatcher
val networkSize = 1
val paymentCount = 10
val channelAmount = 10000000000L.msats
val outputFileName = "test.csv"
// release
val EclairVersion = Option.empty[String]
val EclairCommit = Option.empty[String]
// psql
// compiled binary can be found here:
// https://s3-us-west-1.amazonaws.com/suredbits.com/eclair/eclair-node-0.3.3-SNAPSHOT-949f1ec-psql.jar
// put it into binaries/eclair/0.3.3-SNAPSHOT directory
// val EclairVersion = Option("0.3.3-SNAPSHOT")
// val EclairCommit = Option("949f1ec-psql")
// don't forget to recreate `eclair` Postgres database before starting a new test
EclairRpcTestUtil.customConfigMap = Map(
"eclair.db.driver" -> "psql"
)
def sendPayments(network: Network, amount: MilliSatoshis, count: Int)(
implicit ec: ExecutionContext): Future[Vector[PaymentId]] =
for {
testNodeInfo <- network.testEclairNode.getInfo
paymentIds <- Future.sequence(network.networkEclairNodes.map { node =>
1.to(count).foldLeft(Future.successful(Vector.empty[PaymentId])) {
(accF, _) =>
for {
acc <- accF
invoice <- network.testEclairNode
.createInvoice("test " + System.currentTimeMillis(), amount)
paymentHash = invoice.lnTags.paymentHash.hash
_ = logPaymentHash(paymentHash)
id <- node.sendToNode(testNodeInfo.nodeId,
invoice.amount.get.toMSat,
invoice.lnTags.paymentHash.hash,
None,
None,
None,
None)
} yield {
logPaymentId(paymentHash, id)
acc :+ id
}
}
})
} yield paymentIds.flatten
def runTests(network: Network): Future[Vector[PaymentLogEntry]] = {
println("Setting up the test network")
for {
_ <- network.testEclairNode.connectToWebSocket(logEvent)
_ = println(
s"Set up ${networkSize} nodes, that will send $paymentCount paments to the test node each")
_ = println(
s"Test node data directory: ${network.testEclairNode.instance.authCredentials.datadir
.getOrElse("")}")
_ = println("Testing...")
_ <- sendPayments(network, 1000.msats, paymentCount)
_ <- TestAsyncUtil.retryUntilSatisfied(
condition = paymentLog.size() == networkSize * paymentCount,
duration = 1.second,
maxTries = 100)
_ <- TestAsyncUtil
.retryUntilSatisfied(condition =
paymentLog.values().asScala.forall(_.completed),
duration = 1.second,
maxTries = 100)
_ = println("Done!")
} yield {
paymentLog
.values()
.asScala
.toVector
.sortBy(_.paymentSentAt)
}
}
val res: Future[Unit] = for {
network <- Network.start(EclairVersion,
EclairCommit,
networkSize,
channelAmount)
log <- runTests(network).recoverWith {
case e: Throwable =>
e.printStackTrace()
Future.successful(Vector.empty[PaymentLogEntry])
}
_ <- network.shutdown()
} yield {
if (log.nonEmpty) {
val first = log.head
val csv =
Vector(
"time,number_of_payments,payment_hash,payment_id,event,payment_sent_at,payment_id_received_at,event_received_at,received_in,completed_in") ++
log.zipWithIndex
.map {
case (x, i) =>
s"${x.paymentSentAt - first.paymentSentAt},${i + 1},${x.toCSV}"
}
val outputFile = new File(outputFileName)
Files.write(outputFile.toPath,
csv.asJava,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)
println(s"The test results was written in ${outputFile.getAbsolutePath}")
}
}
res.onComplete { e =>
e match {
case Success(_) => ()
case Failure(ex) => ex.printStackTrace()
}
sys.exit()
}
}

View file

@ -0,0 +1,119 @@
package org.bitcoins.bench.eclair
import java.util.concurrent.ConcurrentHashMap
import java.util.function.BiFunction
import org.bitcoins.core.crypto.Sha256Digest
import org.bitcoins.eclair.rpc.api.WebSocketEvent.{
PaymentFailed,
PaymentReceived,
PaymentSent
}
import org.bitcoins.eclair.rpc.api.{PaymentId, WebSocketEvent}
object PaymentLog {
case class PaymentLogEntry(
paymentHash: Option[Sha256Digest] = None,
id: Option[PaymentId] = None,
event: Option[WebSocketEvent] = None,
paymentSentAt: Long,
paymentIdReceivedAt: Long = 0,
eventReceivedAt: Long = 0) {
def withPaymentHash(paymentHash: Sha256Digest): PaymentLogEntry =
copy(paymentHash = Some(paymentHash),
paymentSentAt = System.currentTimeMillis())
def withPaymentId(id: PaymentId): PaymentLogEntry =
copy(id = Some(id), paymentIdReceivedAt = System.currentTimeMillis())
def withEvent(e: WebSocketEvent): PaymentLogEntry =
copy(event = Some(e), eventReceivedAt = System.currentTimeMillis())
def completed: Boolean = event.isDefined
def completedAt: Long = event match {
case None => 0
case Some(e) =>
e match {
case PaymentReceived(_, parts) =>
parts.maxBy(_.timestamp).timestamp.toMillis
case PaymentFailed(_, _, _, timestamp) => timestamp.toMillis
case _: WebSocketEvent =>
throw new RuntimeException("Can't extract a timestamp")
}
}
def toCSV: String =
s"""${paymentHash
.map(_.hex)
.getOrElse("")},${id.map(_.toString).getOrElse("")},${event
.map(_.getClass.getName.split('$').last)
.getOrElse("")},$paymentSentAt,$paymentIdReceivedAt,$eventReceivedAt,${paymentIdReceivedAt - paymentSentAt},${eventReceivedAt - paymentIdReceivedAt}"""
}
object PaymentLogEntry {
def apply(paymentHash: Sha256Digest): PaymentLogEntry = {
PaymentLogEntry(paymentSentAt = System.currentTimeMillis(),
paymentHash = Some(paymentHash))
}
}
val paymentLog =
new ConcurrentHashMap[Sha256Digest, PaymentLogEntry]()
def logPaymentHash(paymentHash: Sha256Digest): PaymentLogEntry = {
paymentLog.putIfAbsent(paymentHash, PaymentLogEntry(paymentHash))
}
def logPaymentId(
paymentHash: Sha256Digest,
paymentId: PaymentId): PaymentLogEntry = {
paymentLog.compute(
paymentHash,
new BiFunction[Sha256Digest, PaymentLogEntry, PaymentLogEntry] {
override def apply(
hash: Sha256Digest,
old: PaymentLogEntry): PaymentLogEntry = {
val log = if (old == null) {
PaymentLogEntry(paymentSentAt = 0, paymentHash = Some(hash))
} else {
old
}
log.withPaymentId(paymentId)
}
}
)
}
def logEvent(event: WebSocketEvent): PaymentLogEntry = {
val hash: Sha256Digest = event match {
case PaymentReceived(paymentHash, _) =>
paymentHash
case PaymentSent(_, paymentHash, _, _) => paymentHash
case PaymentFailed(_, paymentHash, _, _) =>
paymentHash
case _: WebSocketEvent =>
throw new RuntimeException("Can't extract payment hash")
}
paymentLog.compute(
hash,
new BiFunction[Sha256Digest, PaymentLogEntry, PaymentLogEntry] {
override def apply(
hash: Sha256Digest,
old: PaymentLogEntry): PaymentLogEntry = {
val log = if (old == null) {
PaymentLogEntry(paymentSentAt = 0, paymentHash = Some(hash))
} else {
old
}
log.withEvent(event)
}
}
)
}
}

View file

@ -294,7 +294,7 @@ lazy val bench = project
name := "bitcoin-s-bench",
skip in publish := true
)
.dependsOn(core % testAndCompile)
.dependsOn(core % testAndCompile, testkit)
lazy val eclairRpcTest = project
.in(file("eclair-rpc-test"))
@ -385,7 +385,7 @@ lazy val keyManagerTest = project
.in(file("key-manager-test"))
.settings(CommonSettings.testSettings: _*)
.settings(name := "bitcoin-s-keymanager-test",
libraryDependencies ++= Deps.keyManagerTest)
libraryDependencies ++= Deps.keyManagerTest)
.dependsOn(keyManager, testkit)
lazy val walletDbSettings = dbFlywaySettings("walletdb")

View file

@ -1,7 +1,6 @@
package org.bitcoins.core.protocol.ln
import org.bitcoins.core.crypto._
import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen
import org.bitcoins.core.number.{UInt32, UInt64, UInt8}
import org.bitcoins.core.protocol.ln.LnParams.{
LnBitcoinMainNet,
@ -19,6 +18,7 @@ import org.bitcoins.core.protocol.ln.fee.{
import org.bitcoins.core.protocol.ln.routing.LnRoute
import org.bitcoins.core.protocol.{Bech32Address, P2PKHAddress, P2SHAddress}
import org.bitcoins.core.util.CryptoUtil
import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen
import org.bitcoins.testkit.util.BitcoinSUnitTest
import scodec.bits.ByteVector
@ -113,13 +113,17 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
val descriptionTagE = Left(LnTag.DescriptionTag("ナンセンス 1杯"))
val expiryTag = LnTag.ExpiryTimeTag(UInt32(60))
val lnTags = LnTaggedFields(paymentTag,
descriptionTagE,
None,
Some(expiryTag),
None,
None,
None)
val lnTags = LnTaggedFields(
paymentHash = paymentTag,
secret = None,
descriptionOrHash = descriptionTagE,
nodeId = None,
expiryTime = Some(expiryTag),
cltvExpiry = None,
fallbackAddress = None,
routingInfo = None,
features = None
)
val signature = ECDigitalSignature.fromRS(
"259f04511e7ef2aa77f6ff04d51b4ae9209504843e5ab9672ce32a153681f687515b73ce57ee309db588a10eb8e41b5a2d2bc17144ddf398033faa49ffe95ae6")
@ -359,6 +363,45 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
deserialized.get must be(lnInvoice)
}
it must "parse BOLT11 example 10" in {
val expected =
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqqq4u9s93jtgysm3mrwll70zr697y3mf902hvxwej0v7c62rsltw83ng0pu8w3j230sluc5gxkdmm9dvpy9y6ggtjd2w544mzdrcs42t7sqdkcy8h"
val signature = ECDigitalSignature.fromHex(
"3045022100af0b02c64b4121b8ec6efffcf10f45f123b495eabb0cecc9ecf634a1c3eb71e30220343c3c3ba32545f0ff31441acddecad60485269085c9aa752b5d89a3c42aa5fa")
val lnInvoiceSig =
LnInvoiceSignature(recoverId = UInt8.zero, signature = signature)
val descriptionTag = Left(LnTag.DescriptionTag("coffee beans"))
val paymentSecret = Some(
LnTag.SecretTag(PaymentSecret.fromHex(
"1111111111111111111111111111111111111111111111111111111111111111")))
val features = Some(
LnTag.FeaturesTag(ByteVector.fromValidHex("800000000000000000000800")))
val lnTags = LnTaggedFields(paymentHash = paymentTag,
descriptionOrHash = descriptionTag,
secret = paymentSecret,
features = features)
val hrpMilli =
LnHumanReadablePart(LnBitcoinMainNet, Some(MilliBitcoins(25)))
val lnInvoice = LnInvoice(hrp = hrpMilli,
timestamp = time,
lnTags = lnTags,
signature = lnInvoiceSig)
val serialized = lnInvoice.toString
serialized must be(expected)
val deserialized = LnInvoice.fromString(serialized)
deserialized.get.toString must be(serialized)
}
it must "deserialize and reserialize a invoice with a explicity expiry time" in {
//from eclair
val bech32 =
@ -449,4 +492,16 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
invoice.nodeId.hex must be(expected)
}
it must "parse secret and features tags" in {
// generated by Eclair 3.3.0-SNAPSHOT
val serialized =
"lnbcrt10n1p0px7lfpp5ghc2y7ttnwy58jx0dfcsdxy7ey0qfryn0wcmm04ckud0qw73kt9sdq9vehk7xqrrss9qypqqqsp5qlf6efygd26y03y66jdqqfmlxthplnu5cc8648fgn88twhpyvmgqg9k5kd0k8vv3xvvqpkhkt9chdl579maq45gvck4g0yd0eggmvfkzgvjmwn29r99p57tgyl3l3s82hlc4e97at55xl5lyzpfk6n36yyqqxeem8q"
val invoice = LnInvoice.fromString(serialized).get
invoice.lnTags.secret must be(
Some(LnTag.SecretTag(PaymentSecret.fromHex(
"07d3aca4886ab447c49ad49a00277f32ee1fcf94c60faa9d2899ceb75c2466d0"))))
invoice.lnTags.features must be(
Some(LnTag.FeaturesTag(ByteVector.fromValidHex("0800"))))
}
}

View file

@ -18,6 +18,9 @@ object LnTagPrefix {
case object PaymentHash extends LnTagPrefix {
override val value: Char = 'p'
}
case object Secret extends LnTagPrefix {
override val value: Char = 's'
}
case object Description extends LnTagPrefix {
override val value: Char = 'd'
}
@ -47,15 +50,21 @@ object LnTagPrefix {
override val value: Char = 'r'
}
case object Features extends LnTagPrefix {
override val value: Char = '9'
}
private lazy val all: Map[Char, LnTagPrefix] =
List(PaymentHash,
Secret,
Description,
NodeId,
DescriptionHash,
ExpiryTime,
CltvExpiry,
FallbackAddress,
RoutingInfo)
RoutingInfo,
Features)
.map(prefix => prefix.value -> prefix)
.toMap

View file

@ -23,6 +23,8 @@ sealed abstract class LnTaggedFields extends NetworkElement {
def paymentHash: LnTag.PaymentHashTag
def secret: Option[LnTag.SecretTag]
def description: Option[LnTag.DescriptionTag]
def nodeId: Option[LnTag.NodeIdTag]
@ -37,14 +39,18 @@ sealed abstract class LnTaggedFields extends NetworkElement {
def routingInfo: Option[LnTag.RoutingInfo]
def features: Option[LnTag.FeaturesTag]
lazy val data: Vector[UInt5] = Vector(Some(paymentHash),
description,
nodeId,
descriptionHash,
secret,
expiryTime,
cltvExpiry,
fallbackAddress,
routingInfo)
routingInfo,
features)
.filter(_.isDefined)
.flatMap(_.get.data)
@ -70,9 +76,11 @@ object LnTaggedFields {
nodeId: Option[LnTag.NodeIdTag],
descriptionHash: Option[LnTag.DescriptionHashTag],
expiryTime: Option[LnTag.ExpiryTimeTag],
secret: Option[LnTag.SecretTag],
cltvExpiry: Option[LnTag.MinFinalCltvExpiry],
fallbackAddress: Option[LnTag.FallbackAddressTag],
routingInfo: Option[LnTag.RoutingInfo])
routingInfo: Option[LnTag.RoutingInfo],
features: Option[LnTag.FeaturesTag])
extends LnTaggedFields
/**
@ -82,11 +90,13 @@ object LnTaggedFields {
def apply(
paymentHash: LnTag.PaymentHashTag,
descriptionOrHash: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag],
secret: Option[LnTag.SecretTag] = None,
nodeId: Option[LnTag.NodeIdTag] = None,
expiryTime: Option[LnTag.ExpiryTimeTag] = None,
cltvExpiry: Option[LnTag.MinFinalCltvExpiry] = None,
fallbackAddress: Option[LnTag.FallbackAddressTag] = None,
routingInfo: Option[LnTag.RoutingInfo] = None): LnTaggedFields = {
routingInfo: Option[LnTag.RoutingInfo] = None,
features: Option[LnTag.FeaturesTag] = None): LnTaggedFields = {
val (description, descriptionHash): (
Option[LnTag.DescriptionTag],
@ -102,13 +112,15 @@ object LnTaggedFields {
InvoiceTagImpl(
paymentHash = paymentHash,
secret = secret,
description = description,
nodeId = nodeId,
descriptionHash = descriptionHash,
expiryTime = expiryTime,
cltvExpiry = cltvExpiry,
fallbackAddress = fallbackAddress,
routingInfo = routingInfo
routingInfo = routingInfo,
features = features
)
}
@ -164,6 +176,8 @@ object LnTaggedFields {
s"Payment hash must be defined in a LnInvoice")
)
val secret = getTag[LnTag.SecretTag]
val description = getTag[LnTag.DescriptionTag]
val descriptionHash = getTag[LnTag.DescriptionHashTag]
@ -178,6 +192,8 @@ object LnTaggedFields {
val routingInfo = getTag[LnTag.RoutingInfo]
val features = getTag[LnTag.FeaturesTag]
val d: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag] = {
if (description.isDefined && descriptionHash.isDefined) {
throw new IllegalArgumentException(
@ -194,12 +210,14 @@ object LnTaggedFields {
LnTaggedFields(
paymentHash = paymentHashTag,
secret = secret,
descriptionOrHash = d,
nodeId = nodeId,
expiryTime = expiryTime,
cltvExpiry = cltvExpiry,
fallbackAddress = fallbackAddress,
routingInfo = routingInfo
routingInfo = routingInfo,
features = features
)
}

View file

@ -128,6 +128,15 @@ object LnTag {
}
}
case class SecretTag(secret: PaymentSecret) extends LnTag {
override val prefix: LnTagPrefix = LnTagPrefix.Secret
override val encoded: Vector[UInt5] = {
Bech32.from8bitTo5bit(secret.bytes)
}
}
case class DescriptionTag(string: String) extends LnTag {
override val prefix: LnTagPrefix = LnTagPrefix.Description
@ -261,6 +270,9 @@ object LnTag {
val hash = Sha256Digest.fromBytes(bytes)
LnTag.PaymentHashTag(hash)
case LnTagPrefix.Secret =>
LnTag.SecretTag(PaymentSecret.fromBytes(bytes))
case LnTagPrefix.Description =>
val description = new String(bytes.toArray, Charset.forName("UTF-8"))
LnTag.DescriptionTag(description)
@ -287,13 +299,26 @@ object LnTag {
case LnTagPrefix.FallbackAddress =>
val version = payload.head.toUInt8
val noVersion = payload.tail
val noVersionBytes = UInt8.toBytes(Bech32.from5bitTo8bit(noVersion))
val noVersionBytes =
UInt8.toBytes(Bech32.from5bitTo8bit(noVersion))
FallbackAddressV.fromU8(version, noVersionBytes, MainNet)
case LnTagPrefix.RoutingInfo =>
RoutingInfo.fromU5s(payload)
case LnTagPrefix.Features =>
LnTag.FeaturesTag(bytes)
}
tag
}
case class FeaturesTag(features: ByteVector) extends LnTag {
override def prefix: LnTagPrefix = LnTagPrefix.Features
/** The payload for the tag without any meta information encoded with it */
override def encoded: Vector[UInt5] = {
Bech32.from8bitTo5bit(features)
}
}
}

View file

@ -0,0 +1,23 @@
package org.bitcoins.core.protocol.ln
import org.bitcoins.core.crypto.{ECPrivateKey, Sha256Digest}
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.util.{CryptoUtil, Factory}
import scodec.bits.ByteVector
final case class PaymentSecret(bytes: ByteVector) extends NetworkElement {
require(bytes.size == 32,
s"Payment secret must be 32 bytes in size, got: " + bytes.length)
lazy val hash: Sha256Digest = CryptoUtil.sha256(bytes)
}
object PaymentSecret extends Factory[PaymentSecret] {
override def fromBytes(bytes: ByteVector): PaymentSecret = {
new PaymentSecret(bytes)
}
def random: PaymentSecret = fromBytes(ECPrivateKey.freshPrivateKey.bytes)
}

View file

@ -302,7 +302,7 @@ sealed abstract class Bech32 {
.map(_.toLower)
.map { char =>
val index = Bech32.charset.indexOf(char)
require(index > 0,
require(index >= 0,
s"$char (${char.toInt}) is not part of the Bech32 charset!")
UInt5(index)
}

View file

@ -3,7 +3,7 @@ package org.bitcoins.eclair.rpc
import java.nio.file.Files
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits, Satoshis}
import org.bitcoins.core.number.{Int64, UInt64}
import org.bitcoins.core.number.UInt64
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.ln.LnParams.LnBitcoinRegTest
import org.bitcoins.core.protocol.ln.channel.{
@ -353,7 +353,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
bitcoind <- EclairRpcTestUtil.startedBitcoindRpcClient()
eclair <- {
val server = EclairRpcTestUtil.eclairInstance(bitcoind)
val eclair = new EclairRpcClient(server, EclairRpcTestUtil.binary)
val eclair =
new EclairRpcClient(server, EclairRpcTestUtil.binary(None, None))
eclair.start().map(_ => eclair)
}
_ <- TestAsyncUtil.retryUntilSatisfiedF(conditionF =
@ -480,7 +481,8 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
val badClientF =
badInstanceF.map(new EclairRpcClient(_, EclairRpcTestUtil.binary))
badInstanceF.map(
new EclairRpcClient(_, EclairRpcTestUtil.binary(None, None)))
badClientF.flatMap { badClient =>
recoverToSucceededIf[RuntimeException](badClient.getInfo)
@ -809,7 +811,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
// We spawn fresh clients in this test because the test
// needs nodes with activity both related and not related
// to them
it should "get all channel updates for a given node ID" in {
it should "get all channel updates for a given node ID" ignore {
val freshClients1F = bitcoindRpcClientF.flatMap { bitcoindRpcClient =>
EclairRpcTestUtil.createNodePair(Some(bitcoindRpcClient))
}
@ -844,8 +846,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
}
def openChannel(c1: EclairRpcClient,
c2: EclairRpcClient): Future[FundedChannelId] = {
def openChannel(
c1: EclairRpcClient,
c2: EclairRpcClient): Future[FundedChannelId] = {
EclairRpcTestUtil
.openChannel(c1, c2, Satoshis(500000), MilliSatoshis(500000))
}
@ -1013,7 +1016,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
)
}
it should "get updates for a single node" in {
it should "get updates for a single node" ignore {
// allupdates for a single node is broken in Eclair 0.3.2
// TODO remove recoverToPendingIf when https://github.com/ACINQ/eclair/issues/1179 is fixed
recoverToPendingIf[RuntimeException](for {
@ -1049,7 +1052,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
ourUpdates.flatMap(our =>
allUpdates.map { all =>
our != all
})
})
}
AsyncUtil
@ -1145,8 +1148,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
}
private def hasConnection(client: Future[EclairRpcClient],
nodeId: NodeId): Future[Assertion] = {
private def hasConnection(
client: Future[EclairRpcClient],
nodeId: NodeId): Future[Assertion] = {
val hasPeersF = client.flatMap(_.getPeers.map(_.nonEmpty))
@ -1161,8 +1165,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
}
/** Checks that the given [[org.bitcoins.eclair.rpc.client.EclairRpcClient]] has the given chanId */
private def hasChannel(client: EclairRpcClient,
chanId: ChannelId): Future[Assertion] = {
private def hasChannel(
client: EclairRpcClient,
chanId: ChannelId): Future[Assertion] = {
val recognizedOpenChannel: Future[Assertion] = {
val chanResultF: Future[ChannelResult] = client.channel(chanId)

View file

@ -285,25 +285,43 @@ object WebSocketEvent {
timestamp: FiniteDuration //milliseconds
) extends WebSocketEvent
// {"type":"payment-received","paymentHash":"e1367ac5f913708f9ecc754c49477db3e7de404de7e921cab2dfe489227e07a7","parts":[{"amount":1000,"fromChannelId":"f59a3347ac6ef95ae4ad3e3777d137f80e02bf0a88d65b88f521676c7c713bf8","timestamp":1578080963457}]}
case class PaymentReceived(
amount: MilliSatoshis,
paymentHash: Sha256Digest,
fromChannelId: FundedChannelId,
parts: Vector[PaymentReceived.Part]
) extends WebSocketEvent
object PaymentReceived {
case class Part(
amount: MilliSatoshis,
fromChannelId: FundedChannelId,
timestamp: FiniteDuration // milliseconds
)
}
case class PaymentFailed(
id: PaymentId,
paymentHash: Sha256Digest,
failures: Vector[String],
timestamp: FiniteDuration // milliseconds
) extends WebSocketEvent
case class PaymentFailed(paymentHash: Sha256Digest, failures: Vector[String])
extends WebSocketEvent
case class PaymentSent(
amount: MilliSatoshis,
feesPaid: MilliSatoshis,
id: PaymentId,
paymentHash: Sha256Digest,
paymentPreimage: PaymentPreimage,
toChannelId: FundedChannelId,
timestamp: FiniteDuration //milliseconds
parts: Vector[PaymentSent.Part]
) extends WebSocketEvent
object PaymentSent {
case class Part(
id: PaymentId,
amount: MilliSatoshis,
feesPaid: MilliSatoshis,
toChannelId: FundedChannelId,
timestamp: FiniteDuration // milliseconds
)
}
case class PaymentSettlingOnchain(
amount: MilliSatoshis,
paymentHash: Sha256Digest,

View file

@ -4,11 +4,15 @@ import java.io.File
import java.nio.file.NoSuchFileException
import java.util.concurrent.atomic.AtomicInteger
import akka.Done
import akka.actor.ActorSystem
import akka.http.javadsl.model.headers.HttpCredentials
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest}
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.util.ByteString
import org.bitcoins.core.crypto.Sha256Digest
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
@ -837,6 +841,45 @@ class EclairRpcClient(val instance: EclairInstance, binary: Option[File] = None)
f
}
def connectToWebSocket[T](f: WebSocketEvent => T): Future[Unit] = {
val incoming: Sink[Message, Future[Done]] =
Sink.foreach[Message] {
case message: TextMessage.Strict =>
val parsed: JsValue = Json.parse(message.text)
val validated: JsResult[WebSocketEvent] =
parsed.validate[WebSocketEvent]
val event = parseResult[WebSocketEvent](validated, parsed, "ws")
f(event)
}
val flow =
Flow.fromSinkAndSource(incoming, Source.maybe)
val uri =
instance.rpcUri.resolve("/ws").toString.replace("http://", "ws://")
instance.authCredentials.bitcoinAuthOpt
val request = WebSocketRequest(
uri,
extraHeaders = Seq(
Authorization(
BasicHttpCredentials("", instance.authCredentials.password))))
val (upgradeResponse, _) = Http().singleWebSocketRequest(request, flow)
val connected = upgradeResponse.map { upgrade =>
if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
Done
} else {
throw new RuntimeException(
s"Connection failed: ${upgrade.response.status}")
}
}
connected.failed.foreach(ex =>
logger.error(s"Cannot connect to web socket $uri ", ex))
connected.map(_ => ())
}
}
object EclairRpcClient {

View file

@ -9,34 +9,7 @@ import org.bitcoins.core.protocol.ln.channel.{ChannelState, FundedChannelId}
import org.bitcoins.core.protocol.ln.currency.{MilliSatoshis, PicoBitcoins}
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.eclair.rpc.api.{
AuditResult,
BaseChannelInfo,
ChannelDesc,
ChannelInfo,
ChannelResult,
ChannelStats,
ChannelUpdate,
GetInfoResult,
Hop,
IncomingPayment,
IncomingPaymentStatus,
InvoiceResult,
NetworkFeesResult,
NodeInfo,
OpenChannelInfo,
OutgoingPayment,
OutgoingPaymentStatus,
PaymentFailure,
PaymentId,
PaymentRequest,
PeerInfo,
ReceivedPayment,
RelayedPayment,
SentPayment,
UsableBalancesResult,
WebSocketEvent
}
import org.bitcoins.eclair.rpc.api._
import org.bitcoins.eclair.rpc.network.PeerState
import org.bitcoins.rpc.serializers.SerializerUtil
import play.api.libs.json._
@ -465,39 +438,66 @@ object JsonReaders {
timestamp)
}
implicit val paymentReceivedEventReads: Reads[
WebSocketEvent.PaymentReceived] = Reads { js =>
implicit val paymentReceivedEventPartReads: Reads[
WebSocketEvent.PaymentReceived.Part] = Reads { js =>
for {
amount <- (js \ "amount").validate[MilliSatoshis]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
} yield WebSocketEvent.PaymentReceived(amount,
paymentHash,
fromChannelId,
timestamp)
} yield WebSocketEvent.PaymentReceived.Part(amount,
fromChannelId,
timestamp)
}
implicit val paymentReceivedEventReads: Reads[
WebSocketEvent.PaymentReceived] = Reads { js =>
for {
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
parts <- (js \ "parts")
.validate[Vector[WebSocketEvent.PaymentReceived.Part]]
} yield WebSocketEvent.PaymentReceived(paymentHash, parts)
}
implicit val paymentFailedEventReads: Reads[WebSocketEvent.PaymentFailed] =
Json.reads[WebSocketEvent.PaymentFailed]
Reads { js =>
for {
id <- (js \ "id").validate[PaymentId]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
failures <- (js \ "failures").validate[Vector[String]]
timestamp <- (js \ "timestamp")
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
} yield WebSocketEvent.PaymentFailed(id, paymentHash, failures, timestamp)
}
implicit val paymentSentEventPartReads: Reads[
WebSocketEvent.PaymentSent.Part] = Reads { js =>
for {
id <- (js \ "id").validate[PaymentId]
amount <- (js \ "amount").validate[MilliSatoshis]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
} yield WebSocketEvent.PaymentSent.Part(id,
amount,
feesPaid,
toChannelId,
timestamp)
}
implicit val paymentSentEventReads: Reads[WebSocketEvent.PaymentSent] =
Reads { js =>
for {
amount <- (js \ "amount").validate[MilliSatoshis]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
id <- (js \ "id").validate[PaymentId]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
paymentPreimage <- (js \ "paymentPreimage").validate[PaymentPreimage]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp")
.validate[FiniteDuration](finiteDurationReadsMilliseconds)
} yield WebSocketEvent.PaymentSent(amount,
feesPaid,
parts <- (js \ "parts")
.validate[Vector[WebSocketEvent.PaymentSent.Part]]
} yield WebSocketEvent.PaymentSent(id,
paymentHash,
paymentPreimage,
toChannelId,
timestamp)
parts)
}
implicit val paymentSettlingOnchainEventReads: Reads[
@ -512,4 +512,20 @@ object JsonReaders {
timestamp)
}
implicit val webSocketEventReads: Reads[WebSocketEvent] =
Reads { js =>
(js \ "type")
.validate[String]
.flatMap {
case "payment-relayed" => js.validate[WebSocketEvent.PaymentRelayed]
case "payment-received" => js.validate[WebSocketEvent.PaymentReceived]
case "payment-failed" =>
js.validate[WebSocketEvent.PaymentFailed]
case "payment-sent" =>
js.validate[WebSocketEvent.PaymentSent]
case "payment-settling-onchain" =>
js.validate[WebSocketEvent.PaymentSettlingOnchain]
}
}
}

View file

@ -1,11 +1,11 @@
package org.bitcoins.testkit.core.gen.ln
import org.bitcoins.core.crypto.ECPrivateKey
import org.bitcoins.testkit.core.gen._
import org.bitcoins.core.number.{UInt64, UInt8}
import org.bitcoins.core.protocol.ln.LnTag.NodeIdTag
import org.bitcoins.core.protocol.ln._
import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.testkit.core.gen._
import org.scalacheck.Gen
sealed abstract class LnInvoiceGen {
@ -48,6 +48,12 @@ sealed abstract class LnInvoiceGen {
}
}
def secret: Gen[LnTag.SecretTag] = {
CryptoGenerators.sha256Digest.map { hash =>
LnTag.SecretTag(PaymentSecret.fromBytes(hash.bytes))
}
}
def descriptionTag: Gen[LnTag.DescriptionTag] = {
StringGenerators.genString.map { description =>
LnTag.DescriptionTag(description)
@ -103,12 +109,14 @@ sealed abstract class LnInvoiceGen {
descOrHashTag <- descriptionOrDescriptionHashTag
} yield LnTaggedFields(
paymentHash = paymentHash,
secret = None,
descriptionOrHash = descOrHashTag,
expiryTime = None,
cltvExpiry = None,
fallbackAddress = None,
nodeId = None,
routingInfo = None
routingInfo = None,
features = None
)
}
@ -116,7 +124,7 @@ sealed abstract class LnInvoiceGen {
for {
paymentHash <- paymentHashTag
descOrHashTag <- descriptionOrDescriptionHashTag
secret <- Gen.option(secret)
//optional fields
expiryTime <- Gen.option(expiryTime)
cltvExpiry <- Gen.option(cltvExpiry)
@ -124,12 +132,14 @@ sealed abstract class LnInvoiceGen {
routes <- Gen.option(routingInfo)
} yield LnTaggedFields(
paymentHash = paymentHash,
secret = secret,
descriptionOrHash = descOrHashTag,
expiryTime = expiryTime,
cltvExpiry = cltvExpiry,
fallbackAddress = fallbackAddress,
nodeId = nodeIdOpt.map(NodeIdTag(_)),
routingInfo = routes
routingInfo = routes,
features = None
)
}
@ -137,7 +147,7 @@ sealed abstract class LnInvoiceGen {
for {
paymentHash <- paymentHashTag
descOrHashTag <- descriptionOrDescriptionHashTag
secret <- secret
//optional fields
expiryTime <- expiryTime
cltvExpiry <- cltvExpiry
@ -145,12 +155,14 @@ sealed abstract class LnInvoiceGen {
routes <- routingInfo
} yield LnTaggedFields(
paymentHash = paymentHash,
secret = Some(secret),
descriptionOrHash = descOrHashTag,
expiryTime = Some(expiryTime),
cltvExpiry = Some(cltvExpiry),
fallbackAddress = Some(fallbackAddress),
nodeId = nodeIdOpt.map(NodeIdTag(_)),
routingInfo = Some(routes)
routingInfo = Some(routes),
features = None
)
}

View file

@ -6,6 +6,7 @@ import java.nio.file.Files
import akka.actor.ActorSystem
import com.typesafe.config.{Config, ConfigFactory}
import org.bitcoins.core.compat.JavaConverters._
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.ln.channel.{
@ -47,18 +48,20 @@ import scala.util.{Failure, Success}
*
*/
trait EclairRpcTestUtil extends BitcoinSLogger {
import org.bitcoins.core.compat.JavaConverters._
/** Directory where sbt downloads Eclair binaries */
private[bitcoins] val binaryDirectory =
BitcoindRpcTestUtil.baseBinaryDirectory.resolve("eclair")
/** Path to Jar downloaded by Eclair, if it exists */
private[bitcoins] lazy val binary: Option[File] = {
private[bitcoins] def binary(
eclairVersionOpt: Option[String],
eclairCommitOpt: Option[String]): Option[File] = {
val path = binaryDirectory
.resolve(EclairRpcClient.version)
.resolve(eclairVersionOpt.getOrElse(EclairRpcClient.version))
.resolve(
s"eclair-node-${EclairRpcClient.version}-${EclairRpcClient.commit}.jar")
s"eclair-node-${eclairVersionOpt.getOrElse(EclairRpcClient.version)}-${eclairCommitOpt
.getOrElse(EclairRpcClient.commit)}.jar")
if (Files.exists(path)) {
Some(path.toFile)
@ -70,7 +73,8 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
def randomDirName: String =
0.until(5).map(_ => scala.util.Random.alphanumeric.head).mkString
def randomEclairDatadir(): File = new File(s"/tmp/${randomDirName}/.eclair/")
def randomEclairDatadir(): File =
new File(s"/tmp/eclair-test/${randomDirName}/.eclair/")
def cannonicalDatadir = new File(s"${System.getenv("HOME")}/.reg_eclair/")
@ -94,7 +98,7 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
}
//cribbed from https://github.com/Christewart/eclair/blob/bad02e2c0e8bd039336998d318a861736edfa0ad/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala#L140-L153
private def commonConfig(
private[rpc] def commonConfig(
bitcoindInstance: BitcoindInstance,
port: Int = RpcUtil.randomPort,
apiPort: Int = RpcUtil.randomPort): Config = {
@ -126,7 +130,6 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
"eclair.router.broadcast-interval" -> "2 second",
"eclair.auto-reconnect" -> false,
"eclair.to-remote-delay-blocks" -> 144,
"eclair.db.driver" -> "org.sqlite.JDBC",
"eclair.db.regtest.url" -> "jdbc:sqlite:regtest/",
"eclair.max-payment-fee" -> 10, // avoid complaints about too high fees
"eclair.alias" -> "suredbits"
@ -181,7 +184,10 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
eclairInstance(datadir)
}
def randomEclairClient(bitcoindRpcOpt: Option[BitcoindRpcClient] = None)(
def randomEclairClient(
bitcoindRpcOpt: Option[BitcoindRpcClient] = None,
eclairVersionOpt: Option[String] = None,
eclairCommitOpt: Option[String] = None)(
implicit system: ActorSystem): Future[EclairRpcClient] = {
import system.dispatcher
val bitcoindRpcF: Future[BitcoindRpcClient] = {
@ -193,17 +199,20 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
}
val randInstanceF = bitcoindRpcF.map(randomEclairInstance(_))
val eclairRpcF = randInstanceF.map(i => new EclairRpcClient(i, binary))
val eclairRpcF = randInstanceF.map(i =>
new EclairRpcClient(i, binary(eclairVersionOpt, eclairCommitOpt)))
val startedF = eclairRpcF.flatMap(_.start())
startedF.flatMap(_ => eclairRpcF)
}
def cannonicalEclairClient()(
def cannonicalEclairClient(
eclairVersionOpt: Option[String] = None,
eclairCommitOpt: Option[String] = None)(
implicit system: ActorSystem): EclairRpcClient = {
val inst = cannonicalEclairInstance()
new EclairRpcClient(inst, binary)
new EclairRpcClient(inst, binary(eclairVersionOpt, eclairCommitOpt))
}
def deleteTmpDir(dir: File): Boolean = {
@ -441,7 +450,12 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
* Creates two Eclair nodes that are connected together and returns their
* respective [[org.bitcoins.eclair.rpc.client.EclairRpcClient EclairRpcClient]]s
*/
def createNodePair(bitcoindRpcClientOpt: Option[BitcoindRpcClient])(
def createNodePair(
bitcoindRpcClientOpt: Option[BitcoindRpcClient],
eclairVersionOpt1: Option[String] = None,
eclairCommitOpt1: Option[String] = None,
eclairVersionOpt2: Option[String] = None,
eclairCommitOpt2: Option[String] = None)(
implicit system: ActorSystem): Future[
(EclairRpcClient, EclairRpcClient)] = {
import system.dispatcher
@ -460,13 +474,15 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
bitcoindRpcClientF.map(EclairRpcTestUtil.eclairInstance(_))
val clientF = e1InstanceF.flatMap { e1 =>
val e = new EclairRpcClient(e1, binary)
val e =
new EclairRpcClient(e1, binary(eclairVersionOpt1, eclairCommitOpt1))
logger.debug(
s"Temp eclair directory created ${e.getDaemon.authCredentials.datadir}")
e.start().map(_ => e)
}
val otherClientF = e2InstanceF.flatMap { e2 =>
val e = new EclairRpcClient(e2, binary)
val e =
new EclairRpcClient(e2, binary(eclairVersionOpt2, eclairCommitOpt2))
logger.debug(
s"Temp eclair directory created ${e.getDaemon.authCredentials.datadir}")
e.start().map(_ => e)
@ -682,9 +698,82 @@ trait EclairRpcTestUtil extends BitcoinSLogger {
}
shutdownF
}
case class Network(
bitcoind: BitcoindRpcClient,
testEclairNode: EclairRpcClient,
networkEclairNodes: Vector[EclairRpcClient],
channelIds: Vector[FundedChannelId]) {
def shutdown()(implicit ec: ExecutionContext): Future[Unit] =
for {
_ <- Future.sequence(networkEclairNodes.map(_.stop()))
_ <- testEclairNode.stop()
_ <- bitcoind.stop()
} yield ()
}
object Network {
def start(
eclairVersion: Option[String],
eclairCommit: Option[String],
networkSize: Int,
channelAmount: MilliSatoshis)(
implicit system: ActorSystem): Future[Network] = {
import system.dispatcher
for {
bitcoind <- startedBitcoindRpcClient()
testEclairInstance = EclairRpcTestUtil.eclairInstance(bitcoind)
testEclairNode = new EclairRpcClient(
testEclairInstance,
binary(eclairVersion, eclairCommit))
_ <- testEclairNode.start()
_ <- awaitEclairInSync(testEclairNode, bitcoind)
networkEclairInstances = 1
.to(networkSize)
.toVector
.map(_ => EclairRpcTestUtil.eclairInstance(bitcoind))
networkEclairNodes = networkEclairInstances.map(
new EclairRpcClient(_,
binary(Some(EclairRpcClient.version),
Some(EclairRpcClient.commit))))
_ <- Future.sequence(networkEclairNodes.map(_.start()))
_ <- Future.sequence(
networkEclairNodes.map(awaitEclairInSync(_, bitcoind)))
_ <- Future.sequence(
networkEclairNodes.map(connectLNNodes(_, testEclairNode)))
channelIds <- networkEclairNodes.foldLeft(
Future.successful(Vector.empty[FundedChannelId])) { (accF, node) =>
for {
acc <- accF
channelId <- openChannel(n1 = node,
n2 = testEclairNode,
amt = channelAmount.toSatoshis,
pushMSat =
MilliSatoshis(channelAmount.toLong / 2))
} yield acc :+ channelId
}
_ <- Future.sequence(
channelIds.map(awaitChannelOpened(testEclairNode, _)))
} yield Network(bitcoind, testEclairNode, networkEclairNodes, channelIds)
}
}
}
object EclairRpcTestUtil extends EclairRpcTestUtil
object EclairRpcTestUtil extends EclairRpcTestUtil {
var customConfigMap: Map[String, Any] = Map.empty
override def commonConfig(
bitcoindInstance: BitcoindInstance,
port: Int,
apiPort: Int): Config =
super
.commonConfig(bitcoindInstance, port, apiPort)
.withFallback(ConfigFactory.parseMap(customConfigMap.asJava))
}
case class EclairNodes4(
c1: EclairRpcClient,