Upgrade Eclair to v0.7.0 (#4308)

* Upgrade Eclair to v0.7.0

* Scala 2.12 compatibility

* reformat
This commit is contained in:
rorp 2022-05-02 07:04:56 -07:00 committed by GitHub
parent ce00d3ac36
commit 3dc709386a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 359 additions and 87 deletions

View file

@ -285,8 +285,11 @@ case class PaymentRequest(
serialized: String, serialized: String,
description: String, description: String,
paymentHash: Sha256Digest, paymentHash: Sha256Digest,
paymentMetadata: String,
expiry: FiniteDuration, //seconds expiry: FiniteDuration, //seconds
amount: Option[MilliSatoshis]) minFinalCltvExpiry: Int,
amount: Option[MilliSatoshis],
features: Features)
sealed trait PaymentType sealed trait PaymentType
@ -295,13 +298,15 @@ object PaymentType extends StringFactory[PaymentType] {
case object Standard extends PaymentType case object Standard extends PaymentType
case object SwapIn extends PaymentType case object SwapIn extends PaymentType
case object SwapOut extends PaymentType case object SwapOut extends PaymentType
case object Placeholder extends PaymentType
override def fromString(str: String): PaymentType = override def fromString(str: String): PaymentType =
str match { str match {
case "Standard" => Standard case "Standard" => Standard
case "SwapIn" => SwapIn case "SwapIn" => SwapIn
case "SwapOut" => SwapOut case "SwapOut" => SwapOut
case _ => throw new RuntimeException(s"Unknown payment type `$str`") case "placeholder" => Placeholder
case _ => throw new RuntimeException(s"Unknown payment type `$str`")
} }
} }
@ -322,6 +327,7 @@ case class OutgoingPayment(
case class IncomingPayment( case class IncomingPayment(
paymentRequest: PaymentRequest, paymentRequest: PaymentRequest,
paymentPreimage: PaymentPreimage, paymentPreimage: PaymentPreimage,
paymentType: PaymentType,
createdAt: Instant, //milliseconds createdAt: Instant, //milliseconds
status: IncomingPaymentStatus) status: IncomingPaymentStatus)

View file

@ -16,6 +16,7 @@ import org.bitcoins.core.protocol.ln.channel._
import org.bitcoins.core.protocol.ln.currency._ import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.{Feature, FeatureSupport, NodeId} import org.bitcoins.core.protocol.ln.node.{Feature, FeatureSupport, NodeId}
import org.bitcoins.core.protocol.ln.routing.{ChannelRoute, NodeRoute, Route}
import org.bitcoins.core.protocol.script.{ import org.bitcoins.core.protocol.script.{
ScriptPubKey, ScriptPubKey,
ScriptSignature, ScriptSignature,
@ -805,7 +806,9 @@ object JsonReaders {
} }
lazy val featuresByName: Map[String, Feature] = lazy val featuresByName: Map[String, Feature] =
Feature.knownFeatures.map(f => (f.rfcName, f)).toMap org.bitcoins.core.protocol.ln.node.Features.knownFeatures
.map(f => (f.rfcName, f))
.toMap
implicit val featureReads: Reads[Feature] = implicit val featureReads: Reads[Feature] =
Reads { jsValue => Reads { jsValue =>
@ -854,7 +857,7 @@ object JsonReaders {
for { for {
signature <- (jsValue \ "signature").validate[ECDigitalSignature] signature <- (jsValue \ "signature").validate[ECDigitalSignature]
features <- (jsValue \ "features").validate[Features] features <- (jsValue \ "features").validate[Features]
timestamp <- (jsValue \ "timestamp") timestamp <- (jsValue \ "timestamp" \ "unix")
.validate[Instant](instantReadsSeconds) .validate[Instant](instantReadsSeconds)
nodeId <- (jsValue \ "nodeId").validate[NodeId] nodeId <- (jsValue \ "nodeId").validate[NodeId]
rgbColor <- (jsValue \ "rgbColor").validate[String] rgbColor <- (jsValue \ "rgbColor").validate[String]
@ -1021,7 +1024,7 @@ object JsonReaders {
signature <- (jsValue \ "signature").validate[ECDigitalSignature] signature <- (jsValue \ "signature").validate[ECDigitalSignature]
chainHash <- (jsValue \ "chainHash").validate[DoubleSha256Digest] chainHash <- (jsValue \ "chainHash").validate[DoubleSha256Digest]
shortChannelId <- (jsValue \ "shortChannelId").validate[ShortChannelId] shortChannelId <- (jsValue \ "shortChannelId").validate[ShortChannelId]
timestamp <- (jsValue \ "timestamp") timestamp <- (jsValue \ "timestamp" \ "unix")
.validate[Instant](instantReadsSeconds) .validate[Instant](instantReadsSeconds)
channelFlags <- (jsValue \ "channelFlags").validate[ChannelFlags] channelFlags <- (jsValue \ "channelFlags").validate[ChannelFlags]
cltvExpiryDelta <- (jsValue \ "cltvExpiryDelta").validate[Int] cltvExpiryDelta <- (jsValue \ "cltvExpiryDelta").validate[Int]
@ -1080,7 +1083,13 @@ object JsonReaders {
} }
implicit val paymentReceivedReads: Reads[IncomingPaymentStatus.Received] = implicit val paymentReceivedReads: Reads[IncomingPaymentStatus.Received] =
Json.reads[IncomingPaymentStatus.Received] Reads { js =>
for {
amount <- (js \ "amount").validate[MilliSatoshis]
receivedAt <- (js \ "receivedAt" \ "unix")
.validate[Instant](instantReadsMilliseconds)
} yield IncomingPaymentStatus.Received(amount, receivedAt)
}
implicit val hopReads: Reads[Hop] = implicit val hopReads: Reads[Hop] =
Json.reads[Hop] Json.reads[Hop]
@ -1091,7 +1100,7 @@ object JsonReaders {
preimage <- (js \ "paymentPreimage").validate[PaymentPreimage] preimage <- (js \ "paymentPreimage").validate[PaymentPreimage]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis] feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
route <- (js \ "route").validate[Seq[Hop]] route <- (js \ "route").validate[Seq[Hop]]
completed <- (js \ "completedAt") completed <- (js \ "completedAt" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield OutgoingPaymentStatus.Succeeded(paymentPreimage = preimage, } yield OutgoingPaymentStatus.Succeeded(paymentPreimage = preimage,
feesPaid = feesPaid, feesPaid = feesPaid,
@ -1152,17 +1161,23 @@ object JsonReaders {
serialized <- (js \ "serialized").validate[String] serialized <- (js \ "serialized").validate[String]
description <- (js \ "serialized").validate[String] description <- (js \ "serialized").validate[String]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest] paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
paymentMetadata <- (js \ "paymentMetadata").validate[String]
expiry <- (js \ "expiry") expiry <- (js \ "expiry")
.validate[FiniteDuration](finiteDurationReadsSeconds) .validate[FiniteDuration](finiteDurationReadsSeconds)
amount <- (js \ "amount").validateOpt[MilliSatoshis] amount <- (js \ "amount").validateOpt[MilliSatoshis]
minFinalCltvExpiry <- (js \ "minFinalCltvExpiry").validate[Int]
features <- (js \ "features").validate[Features]
} yield PaymentRequest(prefix, } yield PaymentRequest(prefix,
timestamp, timestamp,
nodeId, nodeId,
serialized, serialized,
description, description,
paymentHash, paymentHash,
paymentMetadata,
expiry, expiry,
amount) minFinalCltvExpiry,
amount,
features)
} }
implicit val paymentSucceededReads: Reads[OutgoingPayment] = Reads { js => implicit val paymentSucceededReads: Reads[OutgoingPayment] = Reads { js =>
@ -1175,7 +1190,7 @@ object JsonReaders {
amount <- (js \ "amount").validate[MilliSatoshis] amount <- (js \ "amount").validate[MilliSatoshis]
recipientAmount <- (js \ "recipientAmount").validate[MilliSatoshis] recipientAmount <- (js \ "recipientAmount").validate[MilliSatoshis]
recipientNodeId <- (js \ "recipientNodeId").validate[NodeId] recipientNodeId <- (js \ "recipientNodeId").validate[NodeId]
createdAt <- (js \ "createdAt") createdAt <- (js \ "createdAt" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
paymentRequest <- (js \ "paymentRequest").validateOpt[PaymentRequest] paymentRequest <- (js \ "paymentRequest").validateOpt[PaymentRequest]
status <- (js \ "status").validate[OutgoingPaymentStatus] status <- (js \ "status").validate[OutgoingPaymentStatus]
@ -1197,11 +1212,13 @@ object JsonReaders {
for { for {
paymentRequest <- (js \ "paymentRequest").validate[PaymentRequest] paymentRequest <- (js \ "paymentRequest").validate[PaymentRequest]
paymentPreimage <- (js \ "paymentPreimage").validate[PaymentPreimage] paymentPreimage <- (js \ "paymentPreimage").validate[PaymentPreimage]
createdAt <- (js \ "createdAt") paymentType <- (js \ "paymentType").validate[PaymentType]
createdAt <- (js \ "createdAt" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
status <- (js \ "status").validate[IncomingPaymentStatus] status <- (js \ "status").validate[IncomingPaymentStatus]
} yield IncomingPayment(paymentRequest, } yield IncomingPayment(paymentRequest,
paymentPreimage, paymentPreimage,
paymentType,
createdAt, createdAt,
status) status)
} }
@ -1243,7 +1260,7 @@ object JsonReaders {
for { for {
amount <- (js \ "amount").validate[MilliSatoshis] amount <- (js \ "amount").validate[MilliSatoshis]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId] fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield ReceivedPayment.Part(amount, fromChannelId, timestamp) } yield ReceivedPayment.Part(amount, fromChannelId, timestamp)
} }
@ -1257,7 +1274,7 @@ object JsonReaders {
amount <- (js \ "amount").validate[MilliSatoshis] amount <- (js \ "amount").validate[MilliSatoshis]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis] feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId] toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield SentPayment.Part(id, amount, feesPaid, toChannelId, timestamp) } yield SentPayment.Part(id, amount, feesPaid, toChannelId, timestamp)
} }
@ -1271,7 +1288,7 @@ object JsonReaders {
paymentHash <- (js \ "paymentHash").validate[Sha256Digest] paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId] fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId] toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield RelayedPayment(amountIn, } yield RelayedPayment(amountIn,
amountOut, amountOut,
@ -1289,7 +1306,7 @@ object JsonReaders {
txId <- (js \ "txId").validate[DoubleSha256DigestBE] txId <- (js \ "txId").validate[DoubleSha256DigestBE]
fee <- (js \ "fee").validate[Satoshis] fee <- (js \ "fee").validate[Satoshis]
txType <- (js \ "txType").validate[String] txType <- (js \ "txType").validate[String]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield NetworkFeesResult(remoteNodeId, } yield NetworkFeesResult(remoteNodeId,
channelId, channelId,
@ -1318,7 +1335,7 @@ object JsonReaders {
paymentHash <- (js \ "paymentHash").validate[Sha256Digest] paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId] fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId] toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentRelayed(amountIn, } yield WebSocketEvent.PaymentRelayed(amountIn,
amountOut, amountOut,
@ -1333,7 +1350,7 @@ object JsonReaders {
for { for {
amount <- (js \ "amount").validate[MilliSatoshis] amount <- (js \ "amount").validate[MilliSatoshis]
fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId] fromChannelId <- (js \ "fromChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentReceived.Part(amount, } yield WebSocketEvent.PaymentReceived.Part(amount,
fromChannelId, fromChannelId,
@ -1355,7 +1372,7 @@ object JsonReaders {
id <- (js \ "id").validate[PaymentId] id <- (js \ "id").validate[PaymentId]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest] paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
failures <- (js \ "failures").validate[Vector[JsObject]] failures <- (js \ "failures").validate[Vector[JsObject]]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentFailed(id, } yield WebSocketEvent.PaymentFailed(id,
paymentHash, paymentHash,
@ -1370,7 +1387,7 @@ object JsonReaders {
amount <- (js \ "amount").validate[MilliSatoshis] amount <- (js \ "amount").validate[MilliSatoshis]
feesPaid <- (js \ "feesPaid").validate[MilliSatoshis] feesPaid <- (js \ "feesPaid").validate[MilliSatoshis]
toChannelId <- (js \ "toChannelId").validate[FundedChannelId] toChannelId <- (js \ "toChannelId").validate[FundedChannelId]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentSent.Part(id, } yield WebSocketEvent.PaymentSent.Part(id,
amount, amount,
@ -1398,7 +1415,7 @@ object JsonReaders {
for { for {
amount <- (js \ "amount").validate[MilliSatoshis] amount <- (js \ "amount").validate[MilliSatoshis]
paymentHash <- (js \ "paymentHash").validate[Sha256Digest] paymentHash <- (js \ "paymentHash").validate[Sha256Digest]
timestamp <- (js \ "timestamp") timestamp <- (js \ "timestamp" \ "unix")
.validate[Instant](instantReadsMilliseconds) .validate[Instant](instantReadsMilliseconds)
} yield WebSocketEvent.PaymentSettlingOnchain(amount, } yield WebSocketEvent.PaymentSettlingOnchain(amount,
paymentHash, paymentHash,
@ -1475,4 +1492,28 @@ object JsonReaders {
} }
} }
implicit val routeReads: Reads[Route] = {
Reads { js =>
for {
amount <- (js \ "amount").validate[MilliSatoshis]
nodeIds <- (js \ "nodeIds").validateOpt[Vector[NodeId]]
shortChannelIds <- (js \ "shortChannelIds")
.validateOpt[Vector[ShortChannelId]]
} yield {
if (shortChannelIds.nonEmpty) {
ChannelRoute(amount, shortChannelIds.get)
} else {
NodeRoute(amount, nodeIds.getOrElse(Vector.empty))
}
}
}
}
implicit val findRouteResultReads: Reads[Vector[Route]] = {
Reads { js =>
(js \ "routes").validate[JsArray].map(_.value.toVector.map(_.as[Route]))
}
}
} }

View file

@ -1,5 +1,8 @@
package org.bitcoins.core.protocol.ln.node package org.bitcoins.core.protocol.ln.node
import org.bitcoins.core.protocol.ln.node.FeatureSupport.{Mandatory, Optional}
import scodec.bits.{BitVector, ByteVector}
sealed trait FeatureSupport sealed trait FeatureSupport
object FeatureSupport { object FeatureSupport {
@ -18,111 +21,321 @@ sealed trait Feature {
def mandatory: Int def mandatory: Int
def optional: Int = mandatory + 1 def optional: Int = mandatory + 1
def supportBit(support: FeatureSupport): Int = def supportBit(support: FeatureSupport): Int = support match {
support match { case Mandatory => mandatory
case FeatureSupport.Mandatory => mandatory case Optional => optional
case FeatureSupport.Optional => optional }
}
override def toString = rfcName override def toString = rfcName
} }
object Feature { /** Feature scope as defined in Bolt 9. */
/** Feature that should be advertised in init messages. */
trait InitFeature extends Feature
case object OptionDataLossProtect extends Feature { /** Feature that should be advertised in node announcements. */
trait NodeFeature extends Feature
/** Feature that should be advertised in invoices. */
trait InvoiceFeature extends Feature
// @formatter:on
case class UnknownFeature(bitIndex: Int)
case class Features[T <: Feature](
activated: Map[T, FeatureSupport],
unknown: Set[UnknownFeature] = Set.empty) {
def isEmpty: Boolean = activated.isEmpty && unknown.isEmpty
def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean =
support match {
case Some(s) => activated.get(feature).contains(s)
case None => activated.contains(feature)
}
def hasPluginFeature(feature: UnknownFeature): Boolean =
unknown.contains(feature)
/** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */
def areSupported(remoteFeatures: Features[T]): Boolean = {
// we allow unknown odd features (it's ok to be odd)
val unknownFeaturesOk = remoteFeatures.unknown.forall(_.bitIndex % 2 == 1)
// we verify that we activated every mandatory feature they require
val knownFeaturesOk = remoteFeatures.activated.forall {
case (_, Optional) => true
case (feature, Mandatory) => hasFeature(feature)
}
unknownFeaturesOk && knownFeaturesOk
}
def initFeatures(): Features[InitFeature] =
Features(activated.collect { case (f: InitFeature, s) => (f, s) }, unknown)
def nodeAnnouncementFeatures(): Features[NodeFeature] =
Features(activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown)
def invoiceFeatures(): Features[InvoiceFeature] = Features(
activated.collect { case (f: InvoiceFeature, s) => (f, s) },
unknown)
def unscoped(): Features[Feature] = Features[Feature](
activated.collect { case (f, s) => (f: Feature, s) },
unknown)
def toByteVector: ByteVector = {
val activatedFeatureBytes = toByteVectorFromIndex(activated.map {
case (feature, support) => feature.supportBit(support)
}.toSet)
val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex))
val maxSize = activatedFeatureBytes.size.max(unknownFeatureBytes.size)
activatedFeatureBytes.padLeft(maxSize) | unknownFeatureBytes.padLeft(
maxSize)
}
private def toByteVectorFromIndex(indexes: Set[Int]): ByteVector = {
if (indexes.isEmpty) return ByteVector.empty
// When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting feature bits.
var buf = BitVector.fill(indexes.max + 1)(high = false).bytes.bits
indexes.foreach { i => buf = buf.set(i) }
buf.reverse.bytes
}
override def toString: String = {
val a = activated
.map { case (feature, support) => feature.rfcName + ":" + support }
.mkString(",")
val u = unknown.map(_.bitIndex).mkString(",")
s"$a" + (if (unknown.nonEmpty) s" (unknown=$u)" else "")
}
}
object Features {
def empty[T <: Feature]: Features[T] =
Features[T](Map.empty[T, FeatureSupport])
def apply[T <: Feature](features: (T, FeatureSupport)*): Features[T] =
Features[T](features.toMap)
def apply(bytes: ByteVector): Features[Feature] = apply(bytes.bits)
def apply(bits: BitVector): Features[Feature] = {
val all = bits.toIndexedSeq.reverse.zipWithIndex.collect {
case (true, idx) if knownFeatures.exists(_.optional == idx) =>
Right((knownFeatures.find(_.optional == idx).get, Optional))
case (true, idx) if knownFeatures.exists(_.mandatory == idx) =>
Right((knownFeatures.find(_.mandatory == idx).get, Mandatory))
case (true, idx) => Left(UnknownFeature(idx))
}
Features[Feature](
activated = all.collect { case Right((feature, support)) =>
feature -> support
}.toMap,
unknown = all.collect { case Left(inf) => inf }.toSet
)
}
case object DataLossProtect
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_data_loss_protect" val rfcName = "option_data_loss_protect"
val mandatory = 0 val mandatory = 0
} }
case object InitialRoutingSync extends Feature { case object InitialRoutingSync extends Feature with InitFeature {
val rfcName = "initial_routing_sync" val rfcName = "initial_routing_sync"
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178 // reserved but not used as per lightningnetwork/lightning-rfc/pull/178
val mandatory = 2 val mandatory = 2
} }
case object OptionUpfrontShutdownScript extends Feature { case object UpfrontShutdownScript
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_upfront_shutdown_script" val rfcName = "option_upfront_shutdown_script"
val mandatory = 4 val mandatory = 4
} }
case object ChannelRangeQueries extends Feature { case object ChannelRangeQueries
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "gossip_queries" val rfcName = "gossip_queries"
val mandatory = 6 val mandatory = 6
} }
case object VariableLengthOnion extends Feature { case object VariableLengthOnion
extends Feature
with InitFeature
with NodeFeature
with InvoiceFeature {
val rfcName = "var_onion_optin" val rfcName = "var_onion_optin"
val mandatory = 8 val mandatory = 8
} }
case object ChannelRangeQueriesExtended extends Feature { case object ChannelRangeQueriesExtended
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "gossip_queries_ex" val rfcName = "gossip_queries_ex"
val mandatory = 10 val mandatory = 10
} }
case object StaticRemoteKey extends Feature { case object StaticRemoteKey
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_static_remotekey" val rfcName = "option_static_remotekey"
val mandatory = 12 val mandatory = 12
} }
case object PaymentSecret extends Feature { case object PaymentSecret
extends Feature
with InitFeature
with NodeFeature
with InvoiceFeature {
val rfcName = "payment_secret" val rfcName = "payment_secret"
val mandatory = 14 val mandatory = 14
} }
case object BasicMultiPartPayment extends Feature { case object BasicMultiPartPayment
extends Feature
with InitFeature
with NodeFeature
with InvoiceFeature {
val rfcName = "basic_mpp" val rfcName = "basic_mpp"
val mandatory = 16 val mandatory = 16
} }
case object Wumbo extends Feature { case object Wumbo extends Feature with InitFeature with NodeFeature {
val rfcName = "option_support_large_channel" val rfcName = "option_support_large_channel"
val mandatory = 18 val mandatory = 18
} }
case object AnchorOutputs extends Feature { case object AnchorOutputs extends Feature with InitFeature with NodeFeature {
val rfcName = "option_anchor_outputs" val rfcName = "option_anchor_outputs"
val mandatory = 20 val mandatory = 20
} }
case object AnchorOutputsZeroFeeHtlcTx extends Feature { case object AnchorOutputsZeroFeeHtlcTx
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_anchors_zero_fee_htlc_tx" val rfcName = "option_anchors_zero_fee_htlc_tx"
val mandatory = 22 val mandatory = 22
} }
case object ShutdownAnySegwit extends Feature { case object ShutdownAnySegwit
extends Feature
with InitFeature
with NodeFeature {
val rfcName = "option_shutdown_anysegwit" val rfcName = "option_shutdown_anysegwit"
val mandatory = 26 val mandatory = 26
} }
case object DualFunding extends Feature with InitFeature with NodeFeature {
val rfcName = "option_dual_fund"
val mandatory = 28
}
case object OnionMessages extends Feature with InitFeature with NodeFeature {
val rfcName = "option_onion_messages"
val mandatory = 38
}
case object ChannelType extends Feature with InitFeature with NodeFeature {
val rfcName = "option_channel_type"
val mandatory = 44
}
case object PaymentMetadata extends Feature with InvoiceFeature {
val rfcName = "option_payment_metadata"
val mandatory = 48
}
case object KeySend extends Feature with NodeFeature {
val rfcName = "keysend"
val mandatory = 54
}
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
// We're not advertising these bits yet in our announcements, clients have to assume support. // We're not advertising these bits yet in our announcements, clients have to assume support.
// This is why we haven't added them yet to `areSupported`. // This is why we haven't added them yet to `areSupported`.
case object TrampolinePayment extends Feature { // The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
val rfcName = "trampoline_payment" // we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely
val mandatory = 50 // deprecate it.
} case object TrampolinePaymentPrototype
extends Feature
case object KeySend extends Feature { with InitFeature
val rfcName = "keysend" with NodeFeature
val mandatory = 54 with InvoiceFeature {
val rfcName = "trampoline_payment_prototype"
val mandatory = 148
} }
val knownFeatures: Set[Feature] = Set( val knownFeatures: Set[Feature] = Set(
OptionDataLossProtect, DataLossProtect,
InitialRoutingSync, InitialRoutingSync,
OptionUpfrontShutdownScript, UpfrontShutdownScript,
ChannelRangeQueries, ChannelRangeQueries,
VariableLengthOnion, VariableLengthOnion,
ChannelRangeQueriesExtended, ChannelRangeQueriesExtended,
PaymentSecret, PaymentSecret,
BasicMultiPartPayment, BasicMultiPartPayment,
Wumbo, Wumbo,
TrampolinePayment,
StaticRemoteKey, StaticRemoteKey,
AnchorOutputs, AnchorOutputs,
AnchorOutputsZeroFeeHtlcTx, AnchorOutputsZeroFeeHtlcTx,
ShutdownAnySegwit, ShutdownAnySegwit,
DualFunding,
OnionMessages,
ChannelType,
PaymentMetadata,
TrampolinePaymentPrototype,
KeySend KeySend
) )
// Features may depend on other features, as specified in Bolt 9.
private val featuresDependency = Map(
ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil),
PaymentSecret -> (VariableLengthOnion :: Nil),
BasicMultiPartPayment -> (PaymentSecret :: Nil),
AnchorOutputs -> (StaticRemoteKey :: Nil),
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
DualFunding -> (AnchorOutputsZeroFeeHtlcTx :: Nil),
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil)
)
case class FeatureException(message: String)
extends IllegalArgumentException(message)
def validateFeatureGraph[T <: Feature](
features: Features[T]): Option[FeatureException] =
featuresDependency.collectFirst {
case (feature, dependencies)
if features.unscoped().hasFeature(feature) && dependencies.exists(d =>
!features.unscoped().hasFeature(d)) =>
FeatureException(
s"$feature is set but is missing a dependency (${dependencies
.filter(d => !features.unscoped().hasFeature(d))
.mkString(" and ")})")
}
/** Returns true if both feature sets are compatible. */
def areCompatible[T <: Feature](
ours: Features[T],
theirs: Features[T]): Boolean =
ours.areSupported(theirs) && theirs.areSupported(ours)
/** returns true if both have at least optional support */
def canUseFeature[T <: Feature](
localFeatures: Features[T],
remoteFeatures: Features[T],
feature: T): Boolean = {
localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature)
}
} }

View file

@ -1,12 +1,21 @@
package org.bitcoins.core.protocol.ln.routing package org.bitcoins.core.protocol.ln.routing
import org.bitcoins.core.protocol.ln.channel.ShortChannelId import org.bitcoins.core.protocol.ln.channel.ShortChannelId
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.ln.node.NodeId import org.bitcoins.core.protocol.ln.node.NodeId
/** Represent differet types of LN routes. Supports node and channel routes. /** Represent differet types of LN routes. Supports node and channel routes.
*/ */
sealed trait Route sealed trait Route {
val amount: MilliSatoshis
}
case class NodeRoute(ids: Vector[NodeId]) extends Route case class NodeRoute(
override val amount: MilliSatoshis,
nodeIds: Vector[NodeId])
extends Route
case class ChannelRoute(ids: Vector[ShortChannelId]) extends Route case class ChannelRoute(
override val amount: MilliSatoshis,
shortChannelIds: Vector[ShortChannelId])
extends Route

View file

@ -14,6 +14,7 @@ import org.bitcoins.core.protocol.ln.channel.{
} }
import org.bitcoins.core.protocol.ln.currency._ import org.bitcoins.core.protocol.ln.currency._
import org.bitcoins.core.protocol.ln.node.NodeId import org.bitcoins.core.protocol.ln.node.NodeId
import org.bitcoins.core.protocol.ln.routing.NodeRoute
import org.bitcoins.core.protocol.ln.{ import org.bitcoins.core.protocol.ln.{
LnHumanReadablePart, LnHumanReadablePart,
LnInvoice, LnInvoice,
@ -168,9 +169,9 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
info.publicAddresses info.publicAddresses
.map(_.getHostString) .map(_.getHostString)
.exists(_.endsWith(".onion"))) .exists(_.endsWith(".onion")))
route <- client1.findRoute(invoice, None) routes <- client1.findRoute(invoice, None)
} yield { } yield {
route.ids.size == 4 routes.head.asInstanceOf[NodeRoute].nodeIds.size == 4
}).recover { }).recover {
case err: RuntimeException case err: RuntimeException
if err.getMessage.contains("route not found") => if err.getMessage.contains("route not found") =>
@ -189,7 +190,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
.flatMap(_.getInfo) .flatMap(_.getInfo)
.flatMap(info => .flatMap(info =>
firstClientF.flatMap(_.findRoute(info.nodeId, MilliSatoshis(100)))) firstClientF.flatMap(_.findRoute(info.nodeId, MilliSatoshis(100))))
.map(route => route.ids.length == 4) .map(route => route.head.asInstanceOf[NodeRoute].nodeIds.length == 4)
.recover { .recover {
case err: RuntimeException case err: RuntimeException
if err.getMessage.contains("route not found") => if err.getMessage.contains("route not found") =>
@ -416,6 +417,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
maxTries = 60) maxTries = 60)
} yield succeed } yield succeed
} }
it should "be able to open and close a channel" in { it should "be able to open and close a channel" in {
val changeAddrF = bitcoindRpcClientF.flatMap(_.getNewAddress) val changeAddrF = bitcoindRpcClientF.flatMap(_.getNewAddress)
@ -567,7 +569,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
invoice <- otherClient.createInvoice("foo", amt, preimage) invoice <- otherClient.createInvoice("foo", amt, preimage)
route <- client.findRoute(otherClientNodeId, amt) route <- client.findRoute(otherClientNodeId, amt)
result <- client.sendToRoute(invoice, result <- client.sendToRoute(invoice,
route, route.head,
amt, amt,
invoice.lnTags.paymentHash.hash, invoice.lnTags.paymentHash.hash,
finalCltvExpiry = 144, finalCltvExpiry = 144,
@ -582,7 +584,7 @@ class EclairRpcClientTest extends BitcoinSAsyncTest {
invoice.lnTags.paymentHash.hash) invoice.lnTags.paymentHash.hash)
_ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client, _ <- EclairRpcTestUtil.awaitUntilPaymentSucceeded(client,
result.parentId) result.parentId)
succeeded <- client.getSentInfo(invoice.lnTags.paymentHash.hash) succeeded <- client.getSentInfo(result.parentId)
} yield { } yield {
assert(otherClientNodeId == invoice.nodeId) assert(otherClientNodeId == invoice.nodeId)
assert(succeeded.nonEmpty) assert(succeeded.nonEmpty)

View file

@ -19,8 +19,8 @@ TaskKeys.downloadEclair := {
Files.createDirectories(binaryDir) Files.createDirectories(binaryDir)
} }
val version = "0.6.2" val version = "0.7.0"
val commit = "6817d6f" val commit = "a804905"
logger.debug(s"(Maybe) downloading Eclair binaries for version: $version") logger.debug(s"(Maybe) downloading Eclair binaries for version: $version")
@ -48,7 +48,7 @@ TaskKeys.downloadEclair := {
.mkString .mkString
val expectedHash = val expectedHash =
"e2407173036d9e2176c129f2328018c543c732a96d1f05c0fb35864c15efc9ba" "482a00cc597fd4cc471a1b4035c72a440a9ab336ef5b9006629d2fd717b223b4"
if (hash.equalsIgnoreCase(expectedHash)) { if (hash.equalsIgnoreCase(expectedHash)) {
logger.info(s"Download complete and verified, unzipping result") logger.info(s"Download complete and verified, unzipping result")

View file

@ -9,7 +9,7 @@ import org.bitcoins.core.protocol.ln.channel.{
} }
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.ln.node.{NodeId, NodeUri} import org.bitcoins.core.protocol.ln.node.{NodeId, NodeUri}
import org.bitcoins.core.protocol.ln.routing.{NodeRoute, Route} import org.bitcoins.core.protocol.ln.routing.Route
import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, PaymentPreimage} import org.bitcoins.core.protocol.ln.{LnInvoice, LnParams, PaymentPreimage}
import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.{Address, BitcoinAddress} import org.bitcoins.core.protocol.{Address, BitcoinAddress}
@ -72,13 +72,15 @@ trait EclairApi {
def close(id: ChannelId, spk: ScriptPubKey): Future[ChannelCommandResult] def close(id: ChannelId, spk: ScriptPubKey): Future[ChannelCommandResult]
def findRoute(nodeId: NodeId, amountMsat: MilliSatoshis): Future[NodeRoute] def findRoute(
nodeId: NodeId,
amountMsat: MilliSatoshis): Future[Vector[Route]]
def findRoute(invoice: LnInvoice): Future[NodeRoute] def findRoute(invoice: LnInvoice): Future[Vector[Route]]
def findRoute( def findRoute(
invoice: LnInvoice, invoice: LnInvoice,
amountMsat: MilliSatoshis): Future[NodeRoute] amountMsat: MilliSatoshis): Future[Vector[Route]]
def forceClose(channelId: ChannelId): Future[ChannelCommandResult] def forceClose(channelId: ChannelId): Future[ChannelCommandResult]

View file

@ -143,30 +143,29 @@ class EclairRpcClient(
override def findRoute( override def findRoute(
nodeId: NodeId, nodeId: NodeId,
amountMsat: MilliSatoshis): Future[NodeRoute] = { amountMsat: MilliSatoshis): Future[Vector[Route]] = {
eclairCall[Vector[NodeId]]( eclairCall[Vector[Route]]("findroutetonode",
"findroutetonode", "nodeId" -> nodeId.toString,
"nodeId" -> nodeId.toString, "amountMsat" -> amountMsat.toBigDecimal.toString)
"amountMsat" -> amountMsat.toBigDecimal.toString).map(NodeRoute.apply)
} }
override def findRoute(invoice: LnInvoice): Future[NodeRoute] = { override def findRoute(invoice: LnInvoice): Future[Vector[Route]] = {
findRoute(invoice, None) findRoute(invoice, None)
} }
override def findRoute( override def findRoute(
invoice: LnInvoice, invoice: LnInvoice,
amount: MilliSatoshis): Future[NodeRoute] = { amount: MilliSatoshis): Future[Vector[Route]] = {
findRoute(invoice, Some(amount)) findRoute(invoice, Some(amount))
} }
def findRoute( def findRoute(
invoice: LnInvoice, invoice: LnInvoice,
amountMsat: Option[MilliSatoshis]): Future[NodeRoute] = { amountMsat: Option[MilliSatoshis]): Future[Vector[Route]] = {
val params = Seq( val params = Seq(
Some("invoice" -> invoice.toString), Some("invoice" -> invoice.toString),
amountMsat.map(x => "amountMsat" -> x.toBigDecimal.toString)).flatten amountMsat.map(x => "amountMsat" -> x.toBigDecimal.toString)).flatten
eclairCall[Vector[NodeId]]("findroute", params: _*).map(NodeRoute.apply) eclairCall[Vector[Route]]("findroute", params: _*)
} }
override def forceClose( override def forceClose(
@ -377,8 +376,8 @@ class EclairRpcClient(
//register callback that publishes a payment to our actor system's //register callback that publishes a payment to our actor system's
//event stream, //event stream,
receivedInfoF.foreach { receivedInfoF.foreach {
case None | case None | Some(
Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Pending)) => IncomingPayment(_, _, _, _, IncomingPaymentStatus.Pending)) =>
if (attempts.incrementAndGet() >= maxAttempts) { if (attempts.incrementAndGet() >= maxAttempts) {
// too many tries to get info about a payment // too many tries to get info about a payment
// either Eclair is down or the payment is still in PENDING state for some reason // either Eclair is down or the payment is still in PENDING state for some reason
@ -513,8 +512,8 @@ class EclairRpcClient(
parentId: Option[PaymentId], parentId: Option[PaymentId],
externalId: Option[String]): Future[SendToRouteResult] = { externalId: Option[String]): Future[SendToRouteResult] = {
val ids = route match { val ids = route match {
case NodeRoute(ids) => "nodeIds" -> ids.mkString(",") case NodeRoute(_, ids) => "nodeIds" -> ids.mkString(",")
case ChannelRoute(ids) => "shortChannelIds" -> ids.mkString(",") case ChannelRoute(_, ids) => "shortChannelIds" -> ids.mkString(",")
} }
val params = Seq( val params = Seq(
"invoice" -> invoice.toString, "invoice" -> invoice.toString,
@ -954,10 +953,10 @@ object EclairRpcClient {
implicit system: ActorSystem) = new EclairRpcClient(instance, binary) implicit system: ActorSystem) = new EclairRpcClient(instance, binary)
/** The current commit we support of Eclair */ /** The current commit we support of Eclair */
private[bitcoins] val commit = "6817d6f" private[bitcoins] val commit = "a804905"
/** The current version we support of Eclair */ /** The current version we support of Eclair */
private[bitcoins] val version = "0.6.2" private[bitcoins] val version = "0.7.0"
/** The bitcoind version that eclair is officially tested & supported with by ACINQ /** The bitcoind version that eclair is officially tested & supported with by ACINQ
* @see https://github.com/ACINQ/eclair/releases/tag/v0.6.2 * @see https://github.com/ACINQ/eclair/releases/tag/v0.6.2

View file

@ -110,16 +110,16 @@ trait EclairRpcTestUtil extends Logging {
"eclair.api.binding-ip" -> "127.0.0.1", "eclair.api.binding-ip" -> "127.0.0.1",
"eclair.api.password" -> "abc123", "eclair.api.password" -> "abc123",
"eclair.api.port" -> apiPort, "eclair.api.port" -> apiPort,
"eclair.mindepth-blocks" -> 2, "eclair.channel.mindepth-blocks" -> 2,
"eclair.max-htlc-value-in-flight-msat" -> 100000000000L, "eclair.channel.max-htlc-value-in-flight-msat" -> 100000000000L,
"eclair.router.broadcast-interval" -> "2 second", "eclair.router.broadcast-interval" -> "2 second",
"eclair.auto-reconnect" -> false, "eclair.auto-reconnect" -> false,
"eclair.to-remote-delay-blocks" -> 144, "eclair.channel.to-remote-delay-blocks" -> 144,
"eclair.db.regtest.url" -> "jdbc:sqlite:regtest/", "eclair.db.regtest.url" -> "jdbc:sqlite:regtest/",
"eclair.max-payment-fee" -> 10, // avoid complaints about too high fees "eclair.max-payment-fee" -> 10, // avoid complaints about too high fees
"eclair.alias" -> "suredbits", "eclair.alias" -> "suredbits",
"eclair.fulfill-safety-before-timeout-blocks" -> 1, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 1,
"eclair.min-final-expiry-delta-blocks" -> 2, "eclair.channel.min-final-expiry-delta-blocks" -> 2,
"eclair.features.keysend" -> "optional" "eclair.features.keysend" -> "optional"
) )
} }