Improve support for plugin-defined features (#3264)
What changed, and why it matters
This commit fixes a bug where Eclair did not correctly recognize custom features added by plugins when checking if another node supports them. Previously, plugin features were treated as 'unknown' and `hasFeature` always returned false for them, even when both nodes supported the same plugin feature. The change stores the raw feature bits after decoding and uses them in feature checks, while removing the old 'UnknownFeature' class. It is a correctness and compatibility improvement rather than a critical security patch, but it could affect whether nodes agree on required features during connection.
Review the updated feature-compatibility logic, especially the new `testSupported` encoded-bit loop, to ensure it does not accidentally accept unsupported mandatory features or reject valid optional ones. Run the updated test suites (FeaturesSpec, PeerConnectionSpec, Bolt11InvoiceSpec) and verify that plugin feature negotiation behaves as intended. Consider whether any downstream consumers depend on the removed `unknown` JSON field.
Security signals we found
Fixes feature-negotiation correctness for plugin-defined features
Changes how unknown/even feature bits are validated during compatibility checks
Removes UnknownFeature class and changes JSON serialization shape
Adds EncodedFeatures to preserve raw feature bits from wire/DB messages
Updates plugin trait to allow mandatory vs optional feature support
Evidence from the diff
The patch refactors the Features class in Eclair: it removes UnknownFeature and adds an EncodedFeatures wrapper that preserves the raw feature bit vector. Features.apply(bytes) now populates both the activated map for known features and encoded_opt with the full bit vector. hasFeature now also consults encoded_opt, fixing the case where plugin features were present in the encoded bits but absent from activated. testSupported was updated to validate mandatory even feature bits against both the local activated map and the local encoded bits. The change also lets plugins declare whether their feature is optional or mandatory, and JSON serialization no longer emits an unknown array. Test fixtures and expectations were updated accordingly.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/Features.scalaeclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scalaeclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scalaeclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scalaInspect captured patch +317 / −259
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
index 12e5127..3035c24 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
@@ -71,8 +71,6 @@ trait PermanentChannelFeature extends InitFeature // <- not in the spec
trait ChannelTypeFeature extends InitFeature
// @formatter:on
-case class UnknownFeature(bitIndex: Int)
-
// @formatter:off
sealed trait FeatureCompatibilityResult {
def areCompatible: Boolean = this == FeatureCompatibilityResult.Compatible
@@ -87,66 +85,132 @@ object FeatureCompatibilityResult {
}
// @formatter:on
-case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) {
+/** NB: features are encoded with the most significant bits first. */
+case class EncodedFeatures(bin: ByteVector) {
+ val isEmpty: Boolean = bin.isEmpty
+
+ def hasFeature(feature: Feature, support: Option[FeatureSupport] = None): Boolean = {
+ support match {
+ case Some(support) => hasFeatureBit(feature.supportBit(support))
+ case None => hasFeature(feature, Some(FeatureSupport.Optional)) || hasFeature(feature, Some(FeatureSupport.Mandatory))
+ }
+ }
- def isEmpty: Boolean = activated.isEmpty && unknown.isEmpty
+ def hasFeatureBit(bitIndex: Long): Boolean = {
+ if (bitIndex < bin.size * 8) {
+ val offset = bitIndex % 8
+ (bin.get(bin.size - 1 - (bitIndex / 8)) & (0x01 << offset.toInt)) != 0
+ } else {
+ false
+ }
+ }
+}
+
+object EncodedFeatures {
+ def fromFeatureBits(featureBits: Set[Int]): EncodedFeatures = {
+ if (featureBits.isEmpty) {
+ EncodedFeatures(ByteVector.empty)
+ } else {
+ // Note that we pad to bytes before setting feature bits (we use a byte encoding on the wire).
+ val byteSize = if ((featureBits.max + 1) % 8 == 0) {
+ featureBits.max + 1
+ } else {
+ featureBits.max + 1 + 8 - ((featureBits.max + 1) % 8)
+ }
+ var buf = BitVector.fill(byteSize)(high = false)
+ // We encode feature bits with the most significant bits first.
+ featureBits.foreach { i => buf = buf.set(byteSize - 1 - i) }
+ EncodedFeatures(buf.bytes)
+ }
+ }
+}
+
+/**
+ * @param activated known features are parsed from the encoded features: note that this will not contain plugin features
+ * sent by a remote node: use [[hasFeature]] to test whether a feature is supported or not instead of
+ * parsing the map directly.
+ * @param encoded_opt only provided when reading encoded features, contains all feature bits.
+ */
+case class Features[T <: Feature](activated: Map[T, FeatureSupport], encoded_opt: Option[EncodedFeatures] = None) {
+
+ def isEmpty: Boolean = activated.isEmpty && encoded_opt.forall(_.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)
+ case Some(s) => activated.get(feature).contains(s) || encoded_opt.exists(_.hasFeature(feature, support))
+ case None => activated.contains(feature) || encoded_opt.exists(_.hasFeature(feature))
}
/** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */
def testSupported(remoteFeatures: Features[T]): FeatureCompatibilityResult = {
- // we allow unknown odd features (it's ok to be odd)
- val incompatibleUnknownFeatures = remoteFeatures.unknown.filter(_.bitIndex % 2 == 0)
- // we verify that we activated every mandatory feature they require
- val incompatibleKnownFeatures = remoteFeatures.activated.filter {
+ // We verify that we activated every mandatory feature they require.
+ val incompatibleFeature_opt = remoteFeatures.activated.find {
case (_, Optional) => false
case (feature, Mandatory) => !hasFeature(feature)
- }.keySet
- val incompatibleFeatures = incompatibleUnknownFeatures.map(u => s"unknown_${u.bitIndex}") ++ incompatibleKnownFeatures.map(_.rfcName)
- if (incompatibleFeatures.isEmpty) FeatureCompatibilityResult.Compatible else FeatureCompatibilityResult.NotCompatible(incompatibleFeatures)
+ }.map(_._1.rfcName)
+ // We also verify encoded features, which may contain plugin features and unknown features.
+ val incompatibleEncodedFeature_opt = remoteFeatures.encoded_opt match {
+ case Some(encoded) =>
+ val activatedMandatoryFeatureBits = activated.keySet.map(_.mandatory.toLong)
+ // We only need to check even feature bits (it's ok to be odd), so we step by 2.
+ (0L until (encoded.bin.size * 8) by 2).find(i => {
+ if (encoded.hasFeatureBit(i)) {
+ // They have set a mandatory feature bit: we must support it as well. Note that if this is a plugin feature,
+ // it may be in the encoded features but not the activated ones if the current object is for remote features.
+ !activatedMandatoryFeatureBits.contains(i) && !encoded_opt.exists(_.hasFeatureBit(i)) && !encoded_opt.exists(_.hasFeatureBit(i + 1))
+ } else {
+ false
+ }
+ }).map(i => s"unknown_$i")
+ case None => None
+ }
+ if (incompatibleFeature_opt.isEmpty && incompatibleEncodedFeature_opt.isEmpty) {
+ FeatureCompatibilityResult.Compatible
+ } else {
+ FeatureCompatibilityResult.NotCompatible(incompatibleFeature_opt.toSet ++ incompatibleEncodedFeature_opt.toSet)
+ }
}
def areSupported(remoteFeatures: Features[T]): Boolean = testSupported(remoteFeatures).areCompatible
- def initFeatures(): Features[InitFeature] = Features(activated.collect { case (f: InitFeature, s) => (f, s) }, unknown)
+ def initFeatures(): Features[InitFeature] = Features(activated.collect { case (f: InitFeature, s) => (f, s) }, encoded_opt)
- def nodeAnnouncementFeatures(): Features[NodeFeature] = Features(activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown)
+ def nodeAnnouncementFeatures(): Features[NodeFeature] = Features(activated.collect { case (f: NodeFeature, s) => (f, s) }, encoded_opt)
- def invoiceFeatures(): Features[InvoiceFeature] = Features(activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown)
+ def invoiceFeatures(): Features[InvoiceFeature] = Features(activated.collect { case (f: InvoiceFeature, s) => (f, s) }, encoded_opt)
- def bolt11Features(): Features[Bolt11Feature] = Features(activated.collect { case (f: Bolt11Feature, s) => (f, s) }, unknown)
+ def bolt11Features(): Features[Bolt11Feature] = Features(activated.collect { case (f: Bolt11Feature, s) => (f, s) }, encoded_opt)
- def bolt12Features(): Features[Bolt12Feature] = Features(activated.collect { case (f: Bolt12Feature, s) => (f, s) }, unknown)
+ def bolt12Features(): Features[Bolt12Feature] = Features(activated.collect { case (f: Bolt12Feature, s) => (f, s) }, encoded_opt)
- def unscoped(): Features[Feature] = Features[Feature](activated.collect { case (f, s) => (f: Feature, s) }, unknown)
+ def unscoped(): Features[Feature] = Features[Feature](activated.collect { case (f, s) => (f: Feature, s) }, encoded_opt)
def add(feature: T, support: FeatureSupport): Features[T] = copy(activated = activated + (feature -> support))
def remove(feature: T): Features[T] = copy(activated = activated - feature)
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)
+ val activatedFeatureBytes = EncodedFeatures.fromFeatureBits(activated.map { case (feature, support) => feature.supportBit(support) }.toSet).bin
+ encoded_opt.map(_.bin) match {
+ case Some(encoded) =>
+ // We combine both sources of feature bits, and we minimally-encode by removing leading zeroes.
+ val maxSize = activatedFeatureBytes.size.max(encoded.size)
+ (activatedFeatureBytes.padLeft(maxSize) | encoded.padLeft(maxSize)).dropWhile(_ == 0)
+ case None => activatedFeatureBytes
+ }
}
- 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 = {
+ activated
+ .map { case (feature, support) => feature.rfcName + ":" + support }
+ .mkString(",")
}
- 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 "")
+ override def equals(obj: Any): Boolean = obj match {
+ case features: Features[_] => this.toByteVector.equals(features.toByteVector)
+ case _ => false
}
+
+ override def hashCode(): Int = toByteVector.hashCode
}
object Features {
@@ -155,18 +219,37 @@ object Features {
def apply[T <: Feature](features: (T, FeatureSupport)*): Features[T] = Features[T](Map.from(features))
- 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))
+ if (bits.isEmpty) {
+ Features.empty
+ } else {
+ // When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting feature bits.
+ val padded = if (bits.size % 8 == 0) {
+ bits
+ } else {
+ bits.padLeft(bits.size + (8 - (bits.size % 8)))
+ }
+ Features(padded.bytes)
+ }
+ }
+
+ def apply(bytes: ByteVector): Features[Feature] = {
+ if (bytes.isEmpty) {
+ Features.empty
+ } else {
+ // We extract all official features we support.
+ val encoded = EncodedFeatures(bytes)
+ val activated = knownFeatures.flatMap {
+ case f if encoded.hasFeatureBit(f.optional) => Some(f -> FeatureSupport.Optional)
+ case f if encoded.hasFeatureBit(f.mandatory) => Some(f -> FeatureSupport.Mandatory)
+ case _ => None
+ }
+ Features[Feature](
+ activated = activated.toMap,
+ // Note that we keep all feature bits to allow checking whether plugin features are activated.
+ encoded_opt = Some(encoded),
+ )
}
- Features[Feature](
- activated = all.collect { case Right((feature, support)) => feature -> support }.toMap,
- unknown = all.collect { case Left(inf) => inf }.toSet
- )
}
def fromConfiguration[T <: Feature](config: Config, validFeatures: Set[T], baseFeatures: Features[T]): Features[T] = Features[T](
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
index f1e392b..31f8215 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
@@ -410,20 +410,21 @@ object NodeParams extends Logging {
require(pluginMessageParams.forall(_.feature.mandatory > 128), "Plugin mandatory feature bit is too low, must be > 128")
require(pluginMessageParams.forall(_.feature.mandatory % 2 == 0), "Plugin mandatory feature bit is odd, must be even")
require(pluginMessageParams.flatMap(_.messageTags).forall(_ > 32768), "Plugin messages tags must be > 32768")
- val pluginFeatureSet = pluginMessageParams.map(_.feature.mandatory).toSet
- require(Features.knownFeatures.map(_.mandatory).intersect(pluginFeatureSet).isEmpty, "Plugin feature bit overlaps with known feature bit")
- require(pluginFeatureSet.size == pluginMessageParams.size, "Duplicate plugin feature bits found")
+ require(Features.knownFeatures.map(_.mandatory).intersect(pluginMessageParams.map(_.feature.mandatory).toSet).isEmpty, "Plugin feature bit overlaps with known feature bit")
+ require(pluginMessageParams.map(_.feature.mandatory).toSet.size == pluginMessageParams.size, "Duplicate plugin feature bits found")
val interceptOpenChannelPlugins = pluginParams.collect { case p: InterceptOpenChannelPlugin => p }
require(interceptOpenChannelPlugins.size <= 1, s"At most one plugin is allowed to intercept channel open messages, but multiple such plugins were registered: ${interceptOpenChannelPlugins.map(_.getClass.getSimpleName).mkString(", ")}. Disable conflicting plugins and restart eclair.")
- val coreAndPluginFeatures: Features[Feature] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature))
+ val pluginFeatures = pluginMessageParams.map(p => p.feature -> p.support).toMap
+ val coreAndPluginFeatures: Features[Feature] = features.copy(activated = features.activated ++ pluginFeatures)
val overrideInitFeatures: Map[PublicKey, Features[InitFeature]] = config.getConfigList("override-init-features").asScala.map { e =>
- val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid")))
- val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: InitFeature => f }, features.initFeatures())
- validateFeatures(f.unscoped())
- p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature])
+ val remoteNodeId = PublicKey(ByteVector.fromValidHex(e.getString("nodeid")))
+ val initFeatures = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: InitFeature => f }, features.initFeatures())
+ validateFeatures(initFeatures.unscoped())
+ val pluginInitFeatures = pluginFeatures.collect { case (f: InitFeature, s) => f -> s }
+ remoteNodeId -> initFeatures.copy(activated = initFeatures.activated ++ pluginInitFeatures)
}.toMap
val socksProxy_opt = parseSocks5ProxyParams(config)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala
index 1dbf77e..64b8c01 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala
@@ -41,8 +41,11 @@ trait CustomFeaturePlugin extends PluginParams {
/** Feature bit that the plugin wants to advertise through Init message. */
def feature: Feature
- /** Plugin feature is always defined as unknown and optional. */
- def pluginFeature: UnknownFeature = UnknownFeature(feature.optional)
+ /**
+ * Whether this feature is optional or mandatory for remote peers. We recommend using [[FeatureSupport.Optional]]
+ * otherwise connection will fail with nodes that don't support this feature.
+ */
+ def support: FeatureSupport
}
/** Parameters for a plugin that defines custom commitment transactions (or non-standard HTLCs). */
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
index 11bb739..6813c9c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
@@ -37,7 +37,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.OfferTypes.Offer
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, FeatureSupport, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature}
+import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, FeatureSupport, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64}
import org.json4s
import org.json4s.JsonAST._
import org.json4s.jackson.Serialization
@@ -229,7 +229,10 @@ object FeatureKeySerializer extends MinimalKeySerializer({ case f: Feature => f.
object FeatureSupportSerializer extends MinimalSerializer({ case s: FeatureSupport => JString(s.toString) })
-object UnknownFeatureSerializer extends MinimalSerializer({ case f: UnknownFeature => JInt(f.bitIndex) })
+// @formatter:off
+private case class FeaturesJson(activated: Map[Feature, FeatureSupport])
+object FeaturesSerializer extends ConvertClassSerializer[Features[Feature]](f => FeaturesJson(f.activated))
+// @formatter:on
object ChannelConfigSerializer extends MinimalSerializer({
case x: ChannelConfig => JArray(x.options.toList.map(o => JString(o.name)))
@@ -422,12 +425,7 @@ object InvoiceSerializer extends MinimalSerializer({
.collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta } // NB: we look at fields directly because the value has a spec-defined default
.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq
val amount = p.amount_opt.map(msat => JField("amount", JLong(msat.toLong))).toSeq
- val features = JField("features", Extraction.decompose(p.features)(
- DefaultFormats +
- FeatureKeySerializer +
- FeatureSupportSerializer +
- UnknownFeatureSerializer
- ))
+ val features = JField("features", Extraction.decompose(p.features)(DefaultFormats + FeatureKeySerializer + FeatureSupportSerializer + FeaturesSerializer))
val paymentMetadata = p.paymentMetadata.map(m => JField("paymentMetadata", JString(m.toHex))).toSeq
val routingInfo = JField("routingInfo", Extraction.decompose(p.routingInfo)(
DefaultFormats +
@@ -458,12 +456,7 @@ object InvoiceSerializer extends MinimalSerializer({
Some(JField("nodeId", JString(p.nodeId.toString()))),
Some(JField("paymentHash", JString(p.paymentHash.toString()))),
p.description.map(string => JField("description", JString(string))),
- Some(JField("features", Extraction.decompose(p.features)(
- DefaultFormats +
- FeatureKeySerializer +
- FeatureSupportSerializer +
- UnknownFeatureSerializer
- ))),
+ Some(JField("features", Extraction.decompose(p.features)(DefaultFormats + FeatureKeySerializer + FeatureSupportSerializer + FeaturesSerializer))),
Some(JField("blindedPaths", JArray(p.blindedPaths.map(path => {
val introductionNode = path.route.firstNodeId.toString
val blindedNodes = path.route.blindedHops
@@ -791,7 +784,7 @@ object JsonSerializers {
OutPointKeySerializer +
FeatureKeySerializer +
FeatureSupportSerializer +
- UnknownFeatureSerializer +
+ FeaturesSerializer +
ChannelConfigSerializer +
ChannelFeaturesSerializer +
OpenChannelResponseSerializer +
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala
index d439135..36bd6fb 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala
@@ -152,7 +152,7 @@ object Bolt11Invoice {
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
Some(Accountable()),
// We want to keep invoices as small as possible, so we explicitly remove unknown features.
- Some(InvoiceFeatures(features.copy(unknown = Set.empty).unscoped()))
+ Some(InvoiceFeatures(features.copy(encoded_opt = None).unscoped()))
).flatten
val routingInfoTags = extraHops.filter(_.nonEmpty).map(RoutingInfo)
defaultTags ++ routingInfoTags
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.json
index 5aa1929..00359d1 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.json
@@ -27,8 +27,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json
index 6d01ae4..00607d6 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json
index 8e4b03c..8f24844 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.json
index 5e71bd0..effb9f2 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.json
@@ -26,8 +26,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"remoteParams" : {
@@ -54,8 +53,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.json
index 7d6286e..aec54a2 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.json
@@ -27,8 +27,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -54,8 +53,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.json
index ebbbb2e..a58fd44 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.json
@@ -27,8 +27,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.json
index 52f5323..396d032 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json
index 6e00845..ed5e877 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.json
index 069f8d6..a45a824 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
@@ -247,8 +245,7 @@
"bitcoinSignature1" : "c354ffd0de3b1ea69cf09ea3918bacf65dc208f059bf5ebe2433328cb6aae8f54d7f4ea787a9eb137f945f30afca3846f0c84febfb02fa9bab390a1bd413fa09",
"bitcoinSignature2" : "ab0bdff42031f002f3b2adf4facf1493dbfc6ea7e5f463c557bf1d252a9f1f331442bde73a7a0472c6b54195cc00921c2f7e492d80523467d6dabdaae50e19e4",
"features" : {
- "activated" : { },
- "unknown" : [ ]
+ "activated" : { }
},
"chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
"shortChannelId" : "400000x42x0",
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.json
index 7a3a08b..ad8e02e 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
@@ -145,8 +143,7 @@
"bitcoinSignature1" : "c5c3a3c582ae450844574694311cc8fda45445246b1c28be736bb306db70afc02172b3562b1b827f37290e8c113cd5617c6bddfa32328d3607845da71ba2905d",
"bitcoinSignature2" : "9786d785aa174393b9aecc32605d89b81ae4f7f7c727f2ee455f774176c0893a2eb330de6ed8b03882459d2734b03653b84636a0b8bcf57d8c1a09c010de4309",
"features" : {
- "activated" : { },
- "unknown" : [ ]
+ "activated" : { }
},
"chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
"shortChannelId" : "400000x42x0",
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.json
index 0117c91..5ae4935 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.json
@@ -27,8 +27,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.json
index fcb84b4..22c05b7 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.json
index 7abd5d5..4a4b9aa 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.json b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.json
index 607511d..3c4eb67 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.json b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.json
index 0bd1a05..c17f677 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"remoteParams" : {
@@ -56,8 +55,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.json
index 869b350..4e7a724 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.json
@@ -27,8 +27,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.json b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.json
index 5ced7c1..6bc8aee 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"remoteParams" : {
@@ -57,8 +56,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.json b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.json
index 7feae98..62e5d66 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -56,8 +55,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.json
index 96ab01a..5f55537 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.json
index e76c48f..cec6b56 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.json
index 603860a..8dbe936 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.json
index 6243f0f..301a5c9 100644
--- a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.json
+++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.json
@@ -28,8 +28,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ 50001 ]
+ }
}
},
"remoteParams" : {
@@ -55,8 +54,7 @@
"option_channel_type" : "mandatory",
"basic_mpp" : "optional",
"gossip_queries" : "optional"
- },
- "unknown" : [ ]
+ }
}
},
"channelFlags" : {
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala
index ea05d22..357b84c 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala
@@ -28,6 +28,21 @@ import scodec.bits._
class FeaturesSpec extends AnyFunSuite {
+ case object PluginFeature1 extends Feature with InitFeature with NodeFeature {
+ val rfcName = "test_feature_1"
+ val mandatory = 37304
+ }
+
+ case object PluginFeature2 extends Feature with InitFeature with NodeFeature {
+ val rfcName = "test_feature_2"
+ val mandatory = 37306
+ }
+
+ case object PluginFeature3 extends Feature with InitFeature with NodeFeature {
+ val rfcName = "test_feature_3"
+ val mandatory = 37308
+ }
+
test("'initial_routing_sync' feature") {
assert(Features(hex"08").hasFeature(InitialRoutingSync, Some(FeatureSupport.Optional)))
assert(!Features(hex"08").hasFeature(InitialRoutingSync, Some(FeatureSupport.Mandatory)))
@@ -96,7 +111,6 @@ class FeaturesSpec extends AnyFunSuite {
bin" 010000000010000000000000" -> true,
bin" 010000000001000000000000" -> true,
)
-
for ((testCase, valid) <- testCases) {
if (valid) {
assert(validateFeatureGraph(Features(testCase)).isEmpty)
@@ -128,7 +142,7 @@ class FeaturesSpec extends AnyFunSuite {
),
TestCase(
Features.empty,
- Features(activated = Map.empty, Set(UnknownFeature(101), UnknownFeature(103))),
+ Features(activated = Map.empty, Some(EncodedFeatures.fromFeatureBits(Set(101, 103)))),
oursSupportTheirs = true,
theirsSupportOurs = true,
compatible = true
@@ -168,7 +182,7 @@ class FeaturesSpec extends AnyFunSuite {
// They have unknown optional features
TestCase(
Features(VariableLengthOnion -> Optional),
- Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(141))),
+ Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Some(EncodedFeatures.fromFeatureBits(Set(141)))),
oursSupportTheirs = true,
theirsSupportOurs = true,
compatible = true
@@ -176,7 +190,7 @@ class FeaturesSpec extends AnyFunSuite {
// They have unknown mandatory features
TestCase(
Features(VariableLengthOnion -> Optional),
- Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(142))),
+ Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Some(EncodedFeatures.fromFeatureBits(Set(142)))),
oursSupportTheirs = false,
theirsSupportOurs = true,
compatible = false
@@ -197,13 +211,44 @@ class FeaturesSpec extends AnyFunSuite {
theirsSupportOurs = false,
compatible = false
),
+ // A plugin feature is supported by both nodes.
+ TestCase(
+ Features(VariableLengthOnion -> Optional, PluginFeature1 -> Optional),
+ Features(Map(VariableLengthOnion -> Optional), encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(PluginFeature1.mandatory)))),
+ oursSupportTheirs = true,
+ theirsSupportOurs = true,
+ compatible = true,
+ ),
+ // They have an unknown mandatory feature.
+ TestCase(
+ Features(Features.knownFeatures.map(f => f -> Optional).toMap),
+ Features(Features.knownFeatures.map(f => f -> Optional).toMap, encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(1105, 1120)))),
+ oursSupportTheirs = false,
+ theirsSupportOurs = true,
+ compatible = false,
+ ),
+ // They have an unknown optional feature.
+ TestCase(
+ Features(Features.knownFeatures.map(f => f -> Optional).toMap),
+ Features(Features.knownFeatures.map(f => f -> Optional).toMap, encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(1105)))),
+ oursSupportTheirs = true,
+ theirsSupportOurs = true,
+ compatible = true,
+ ),
+ // We both have an unknown mandatory feature.
+ TestCase(
+ Features(Map.empty, encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(1101, 1136, 1353)))),
+ Features(Map.empty, encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(1105, 1136, 1341)))),
+ oursSupportTheirs = true,
+ theirsSupportOurs = true,
+ compatible = true,
+ ),
// nonreg testing of future features (needs to be updated with every new supported mandatory bit)
- TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
- TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
- TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
- TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
+ TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Some(EncodedFeatures.fromFeatureBits(Set(1024)))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
+ TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Some(EncodedFeatures.fromFeatureBits(Set(1025)))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
+ TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Some(EncodedFeatures.fromFeatureBits(Set(1028)))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
+ TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Some(EncodedFeatures.fromFeatureBits(Set(1029)))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
)
-
for (testCase <- testCases) {
assert(areCompatible(testCase.ours, testCase.theirs) == testCase.compatible, testCase)
assert(testCase.ours.areSupported(testCase.theirs) == testCase.oursSupportTheirs, testCase)
@@ -211,22 +256,33 @@ class FeaturesSpec extends AnyFunSuite {
}
}
+ test("unknown plugin features") {
+ val features = Features(Map[Feature, FeatureSupport](PluginFeature1 -> Optional), Some(EncodedFeatures.fromFeatureBits(Set(PluginFeature2.mandatory))))
+ assert(features.hasFeature(PluginFeature1))
+ assert(features.hasFeature(PluginFeature1, Some(Optional)))
+ assert(!features.hasFeature(PluginFeature1, Some(Mandatory)))
+ assert(features.hasFeature(PluginFeature2))
+ assert(features.hasFeature(PluginFeature2, Some(Mandatory)))
+ assert(!features.hasFeature(PluginFeature2, Some(Optional)))
+ assert(!features.hasFeature(PluginFeature3))
+ }
+
test("filter features based on their usage") {
val features = Features(
Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
- Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
+ Some(EncodedFeatures.fromFeatureBits(Set(753, 852, 65303))),
)
assert(features.initFeatures() == Features(
Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory),
- Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
+ Some(EncodedFeatures.fromFeatureBits(Set(753, 852, 65303))),
))
assert(features.nodeAnnouncementFeatures() == Features(
Map(DataLossProtect -> Optional, VariableLengthOnion -> Mandatory),
- Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
+ Some(EncodedFeatures.fromFeatureBits(Set(753, 852, 65303))),
))
assert(features.invoiceFeatures() == Features(
Map(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
- Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
+ Some(EncodedFeatures.fromFeatureBits(Set(753, 852, 65303))),
))
}
@@ -236,9 +292,8 @@ class FeaturesSpec extends AnyFunSuite {
hex"0100" -> Features(VariableLengthOnion -> Mandatory),
hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional),
hex"09004200" -> Features(Map(VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, RouteBlinding -> Mandatory, ShutdownAnySegwit -> Optional)),
- hex"80010080000000000000000000000000000000000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(151), UnknownFeature(160), UnknownFeature(175)))
+ hex"80010080000000000000000000000000000000000000" -> Features(Map.empty[Feature, FeatureSupport], Some(EncodedFeatures.fromFeatureBits(Set(151, 160, 175))))
)
-
for ((bin, features) <- testCases) {
assert(features.toByteVector == bin)
assert(Features(bin) == features)
@@ -260,7 +315,6 @@ class FeaturesSpec extends AnyFunSuite {
payment_secret = optional
basic_mpp = optional
""")
-
val features = fromConfiguration(conf)
assert(features.toByteVector == hex"028a8a")
assert(Features(hex"028a8a") == features)
@@ -273,7 +327,6 @@ class FeaturesSpec extends AnyFunSuite {
assert(features.hasFeature(PaymentSecret, Some(Optional)))
assert(features.hasFeature(BasicMultiPartPayment, Some(Optional)))
}
-
{
val conf = ConfigFactory.parseString(
"""
@@ -283,7 +336,6 @@ class FeaturesSpec extends AnyFunSuite {
gossip_queries_ex = mandatory
var_onion_optin = optional
""")
-
val features = fromConfiguration(conf)
assert(features.toByteVector == hex"068a")
assert(Features(hex"068a") == features)
@@ -296,7 +348,6 @@ class FeaturesSpec extends AnyFunSuite {
assert(features.hasFeature(VariableLengthOnion, Some(Optional)))
assert(!features.hasFeature(PaymentSecret))
}
-
{
val confWithUnknownFeatures = ConfigFactory.parseString(
"""
@@ -304,10 +355,8 @@ class FeaturesSpec extends AnyFunSuite {
gossip_queries = optional
payment_secret = mandatory
""")
-
assertThrows[RuntimeException](fromConfiguration(confWithUnknownFeatures))
}
-
{
val confWithUnknownSupport = ConfigFactory.parseString(
"""
@@ -315,10 +364,8 @@ class FeaturesSpec extends AnyFunSuite {
gossip_queries = optional
payment_secret = mandatory
""")
-
assertThrows[RuntimeException](fromConfiguration(confWithUnknownSupport))
}
-
{
val confWithDisabledFeatures = ConfigFactory.parseString(
"""
@@ -328,7 +375,6 @@ class FeaturesSpec extends AnyFunSuite {
option_support_large_channel = disabled
gossip_queries_ex = mandatory
""")
-
val features = fromConfiguration(confWithDisabledFeatures)
assert(!features.hasFeature(DataLossProtect))
assert(!features.hasFeature(Wumbo))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala
index 3f6febd..1ac6bd2 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala
@@ -61,7 +61,7 @@ object TestConstants {
val emptyOnionPacket: OnionRoutingPacket = OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes)
val emptyOrigin: Origin.Hot = Origin.Hot(ActorRef.noSender, Upstream.Local(UUID.randomUUID()))
- case object TestFeature extends Feature with InitFeature with NodeFeature {
+ case object PluginFeature extends Feature with InitFeature with NodeFeature {
val rfcName = "test_feature"
val mandatory = 50000
}
@@ -69,7 +69,8 @@ object TestConstants {
val pluginParams: CustomFeaturePlugin = new CustomFeaturePlugin {
// @formatter:off
override def messageTags: Set[Int] = Set(60003)
- override def feature: Feature = TestFeature
+ override def feature: Feature = PluginFeature
+ override def support: FeatureSupport = FeatureSupport.Optional
override def name: String = "plugin for testing"
// @formatter:on
}
@@ -101,7 +102,7 @@ object TestConstants {
publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil,
torAddress_opt = None,
features = Features(
- Map(
+ Map[Feature, FeatureSupport](
Features.DataLossProtect -> FeatureSupport.Optional,
Features.ChannelRangeQueries -> FeatureSupport.Optional,
Features.ChannelRangeQueriesExtended -> FeatureSupport.Optional,
@@ -117,9 +118,9 @@ object TestConstants {
Features.Quiescence -> FeatureSupport.Optional,
Features.SplicePrototype -> FeatureSupport.Optional,
Features.ProvideStorage -> FeatureSupport.Optional,
- Features.ChannelType -> FeatureSupport.Mandatory
- ),
- unknown = Set(UnknownFeature(TestFeature.optional))
+ Features.ChannelType -> FeatureSupport.Mandatory,
+ PluginFeature -> FeatureSupport.Optional
+ )
),
pluginParams = List(pluginParams),
overrideInitFeatures = Map.empty,
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala
index a70b5ad..e82e02e 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala
@@ -56,7 +56,7 @@ class PeersDbSpec extends AnyFunSuite {
val peer1b = TestCase(peer1a.nodeId, peer1a.nodeInfo.copy(address_opt = NodeAddress.fromParts("127.0.0.1", 1112).toOption))
val peer1c = TestCase(peer1a.nodeId, peer1b.nodeInfo.copy(features = Features(Features.ChannelType -> FeatureSupport.Mandatory, Features.ShutdownAnySegwit -> FeatureSupport.Optional)))
val peer2a = TestCase(randomKey().publicKey, NodeInfo(Features.empty, None))
- val peer2b = TestCase(peer2a.nodeId, NodeInfo(Features(Map[InitFeature, FeatureSupport](Features.DataLossProtect -> FeatureSupport.Optional), Set(UnknownFeature(61317))), Some(Tor2("z4zif3fy7fe7bpg3", 4231))))
+ val peer2b = TestCase(peer2a.nodeId, NodeInfo(Features(Map[InitFeature, FeatureSupport](Features.DataLossProtect -> FeatureSupport.Optional), Some(EncodedFeatures.fromFeatureBits(Set(61317)))), Some(Tor2("z4zif3fy7fe7bpg3", 4231))))
val peer3 = TestCase(randomKey().publicKey, NodeInfo(Features.empty, Some(Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231))))
assert(db.listPeers().isEmpty)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
index 6f4954b..bb050bf 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
@@ -37,7 +37,7 @@ import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel
import fr.acinq.eclair.transactions.Transactions.{ClosingTx, InputInfo}
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec
import fr.acinq.eclair.wire.protocol.{ChannelReestablish, Error, IPAddress, LiquidityAds, NodeAddress, OpenChannel, Shutdown, TlvStream}
-import fr.acinq.eclair.{AcceptOpenChannel, BlockHeight, FeatureSupport, Features, InitFeature, InterceptOpenChannelCommand, InterceptOpenChannelPlugin, InterceptOpenChannelReceived, MilliSatoshiLong, RejectOpenChannel, TestConstants, UnknownFeature, randomBytes32, randomKey}
+import fr.acinq.eclair.{AcceptOpenChannel, BlockHeight, EncodedFeatures, FeatureSupport, Features, InitFeature, InterceptOpenChannelCommand, InterceptOpenChannelPlugin, InterceptOpenChannelReceived, MilliSatoshiLong, RejectOpenChannel, TestConstants, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.ByteVector
@@ -266,7 +266,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
}
// They want to use a channel type we don't support yet.
{
- val open = createOpenChannelMessage(UnsupportedChannelType(Features(activated = Map.empty, unknown = Set(UnknownFeature(120)))))
+ val open = createOpenChannelMessage(UnsupportedChannelType(Features(activated = Map.empty, encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(120))))))
openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), defaultFeatures, defaultFeatures, peerConnection.ref, remoteAddress)
peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=0x01000000000000000000000000000000"), peerConnection.ref.toClassic))
eventListener.expectMessageType[ChannelAborted]
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala
index 33801cd..26aaccb 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala
@@ -155,7 +155,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value)
transport.expectMsgType[TransportHandler.ReadAck]
probe.expectTerminated(transport.ref)
- origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (option_channel_type,option_static_remotekey,var_onion_optin,unknown_32,payment_secret)"))
+ origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (unknown_32,var_onion_optin)"))
peer.expectMsg(ConnectionDown(peerConnection))
}
@@ -172,7 +172,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value)
transport.expectMsgType[TransportHandler.ReadAck]
probe.expectTerminated(transport.ref)
- origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (option_channel_type,option_static_remotekey,var_onion_optin,unknown_32,payment_secret)"))
+ origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (unknown_32,var_onion_optin)"))
peer.expectMsg(ConnectionDown(peerConnection))
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala
index 241bad4..f85807d 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala
@@ -167,7 +167,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat
| "initialRequestedChannelReserve_opt": 1000,
| "isChannelOpener": true,
| "paysCommitTxFees" : true,
- | "initFeatures": { "activated": {}, "unknown": [] }
+ | "initFeatures": { "activated": {} }
| },
| "remoteParams": {
| "nodeId": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
@@ -176,7 +176,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat
| "paymentBasepoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
| "delayedPaymentBasepoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
| "htlcBasepoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
- | "initFeatures": { "activated": {}, "unknown": [] }
+ | "initFeatures": { "activated": {} }
| },
| "channelFlags": {
| "nonInitiatorPaysCommitFees": false,
@@ -310,37 +310,33 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat
Features.PaymentSecret -> FeatureSupport.Mandatory,
Features.StaticRemoteKey -> FeatureSupport.Optional
),
- unknown = Set(
- UnknownFeature(457),
- UnknownFeature(5000),
- )
+ encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(457, 5000)))
)
-
- JsonSerializers.serialization.write(features)(JsonSerializers.formats) shouldBe """{"activated":{"initial_routing_sync":"optional","payment_secret":"mandatory","option_static_remotekey":"optional"},"unknown":[457,5000]}"""
+ JsonSerializers.serialization.write(features)(JsonSerializers.formats) shouldBe """{"activated":{"initial_routing_sync":"optional","payment_secret":"mandatory","option_static_remotekey":"optional"}}"""
}
test("Bolt 11 invoice") {
val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy"
val pr = Invoice.fromString(ref).get
- JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}"""
+ JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"}},"routingInfo":[]}"""
}
test("Bolt 11 invoice with routing hints") {
val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an"
val pr = Invoice.fromString(ref).get
- JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
+ JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"basic_mpp":"optional","var_onion_optin":"optional","payment_secret":"optional"}},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
}
test("Bolt 11 invoice with metadata") {
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0"
val pr = Invoice.fromString(ref).get
- JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}"""
+ JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"option_payment_metadata":"mandatory","payment_secret":"mandatory","var_onion_optin":"mandatory"}},"routingInfo":[]}"""
}
test("Bolt 12 invoice") {
val ref = "lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4q2rd3ny0elv9m7mh38xxwe6ypfheeqeqlwgft05r6dhc50gtw0nv2qgrrl9x2qzzqvwukam32mhkdqrvwwcp5l6jcnnnezdq69vz8gdvvgmsqwk3efqf3f6gmf0ul63940awz429rdhhsts86s0r30e5nffwhrqw90xgxf7f60sm7tcclvyqwz7cer5q9223madstdy2p5q6y8qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf2qheqqqqq2gprrgshynfszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsx4k5r7rsld3hhe87psyy5cnhhzt4dz838f75734mted7pdsrflpvys23tkafmhctf3musnsaa42h6qjdggyqlhtevutzzpzlnwd8alq"
val pr = Invoice.fromString(ref).get
- JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"amount":456001234,"nodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","paymentHash":"2cb0e7b052366787450c33daf6d2f2c3cb6132221326e1c1b49ac97fdd7eb720","description":"minimal offer","features":{"activated":{},"unknown":[]},"blindedPaths":[{"introductionNodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","blindedNodeIds":["031fca650042031dcb777156ef66806c73b01a7f52c4e73c89a0d15823a1ac6237"]}],"createdAt":1665412681,"expiresAt":1665412981,"serialized":"lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4q2rd3ny0elv9m7mh38xxwe6ypfheeqeqlwgft05r6dhc50gtw0nv2qgrrl9x2qzzqvwukam32mhkdqrvwwcp5l6jcnnnezdq69vz8gdvvgmsqwk3efqf3f6gmf0ul63940awz429rdhhsts86s0r30e5nffwhrqw90xgxf7f60sm7tcclvyqwz7cer5q9223madstdy2p5q6y8qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf2qheqqqqq2gprrgshynfszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsx4k5r7rsld3hhe87psyy5cnhhzt4dz838f75734mted7pdsrflpvys23tkafmhctf3musnsaa42h6qjdggyqlhtevutzzpzlnwd8alq"}"""
+ JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"amount":456001234,"nodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","paymentHash":"2cb0e7b052366787450c33daf6d2f2c3cb6132221326e1c1b49ac97fdd7eb720","description":"minimal offer","features":{"activated":{}},"blindedPaths":[{"introductionNodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","blindedNodeIds":["031fca650042031dcb777156ef66806c73b01a7f52c4e73c89a0d15823a1ac6237"]}],"createdAt":1665412681,"expiresAt":1665412981,"serialized":"lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4q2rd3ny0elv9m7mh38xxwe6ypfheeqeqlwgft05r6dhc50gtw0nv2qgrrl9x2qzzqvwukam32mhkdqrvwwcp5l6jcnnnezdq69vz8gdvvgmsqwk3efqf3f6gmf0ul63940awz429rdhhsts86s0r30e5nffwhrqw90xgxf7f60sm7tcclvyqwz7cer5q9223madstdy2p5q6y8qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf2qheqqqqq2gprrgshynfszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsx4k5r7rsld3hhe87psyy5cnhhzt4dz838f75734mted7pdsrflpvys23tkafmhctf3musnsaa42h6qjdggyqlhtevutzzpzlnwd8alq"}"""
}
test("Bolt 12 offer") {
@@ -364,7 +360,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat
OfferTypes.OfferQuantityMax(5),
OfferTypes.OfferNodeId(PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")),
), Set(GenericTlv(UInt64(71), hex"bd4e85ce"))))
- JsonSerializers.serialization.write(bigOffer)(JsonSerializers.formats) shouldBe """{"chains":["43f08bdab050e35b567c864b91f47f50ae725ae2de53bcfbbaf284da00000000"],"amount":"862.05","currency":"EUR","description":"offer with a lot of fields in it","expiry":{"iso":"1970-01-01T01:00:00Z","unix":3600},"issuer":"bob@bobcorp.com","nodeId":"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f","paths":[{"firstNodeId":{"publicKey":"022812e3a3760ac989b8749ee9fc70fd12e4d7f3cad5e3e2bf572e9e4eaaa7b7d9"},"length":1}],"quantityMax":5,"features":{"activated":{"option_provide_storage":"mandatory"},"unknown":[]},"metadata":"d5f4a6","unknownTlvs":{"71":"bd4e85ce"}}"""
+ JsonSerializers.serialization.write(bigOffer)(JsonSerializers.formats) shouldBe """{"chains":["43f08bdab050e35b567c864b91f47f50ae725ae2de53bcfbbaf284da00000000"],"amount":"862.05","currency":"EUR","description":"offer with a lot of fields in it","expiry":{"iso":"1970-01-01T01:00:00Z","unix":3600},"issuer":"bob@bobcorp.com","nodeId":"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f","paths":[{"firstNodeId":{"publicKey":"022812e3a3760ac989b8749ee9fc70fd12e4d7f3cad5e3e2bf572e9e4eaaa7b7d9"},"length":1}],"quantityMax":5,"features":{"activated":{"option_provide_storage":"mandatory"}},"metadata":"d5f4a6","unknownTlvs":{"71":"bd4e85ce"}}"""
}
test("Bolt 12 offer data") {
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala
index 8b5e2d6..c307206 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala
@@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, BtcDouble, ByteVector32,
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
import fr.acinq.eclair.payment.Bolt11Invoice._
-import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32}
+import fr.acinq.eclair.{CltvExpiryDelta, EncodedFeatures, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomBytes32}
import org.scalatest.TryValues.convertTryToSuccessOrFailure
import org.scalatest.funsuite.AnyFunSuite
import scodec.DecodeResult
@@ -506,30 +506,29 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
Features(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
- Features(bin" 01000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
+ Features(bin" 00000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
Features(bin" 0000010000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
- Features(bin" 0000011000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
- Features(bin" 0000110000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
- Features(bin" 0000100000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
- Features(bin" 0010000000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
- Features(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
+ Features(bin" 0001010000110000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
+ Features(bin" 0001000000110000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
+ Features(bin" 0100000000110000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
+ Features(bin" 000010000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
// those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit)
- Features(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
- Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
+ Features(bin" 000010000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
+ Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false),
+ Features(bin"00000100000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
Features(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false)
)
-
for ((features, res) <- featureBits) {
val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features)
assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) == res)
- assert(Bolt11Invoice.fromString(invoice.toString).get == invoice)
+ assert(Bolt11Invoice.fromString(invoice.toString).get.toString == invoice.toString)
}
}
test("feature bits to minimally-encoded feature bytes") {
// Invoice features are encoded as 5-bits chunks, which we decode to bytes.
val testCases = Seq(
- (bin" 0010000100000101", hex" 2105"),
+ (bin" 010000100000101", hex" 2105"),
(bin" 1010000100000101", hex" a105"),
(bin" 11000000000000110", hex"018006"),
(bin" 01000000000000110", hex" 8006"),
@@ -538,7 +537,6 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
(bin"0101010000000000110", hex"02a006"),
(bin"1000110000000000110", hex"046006")
)
-
for ((invoiceFeatureBits, featureBytes) <- testCases) {
assert(Features(invoiceFeatureBits).toByteVector == featureBytes)
}
@@ -654,13 +652,10 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
"lnbc100n1pw9qjdgsp5hxzeu9dtpxukstuadtlyhejc24h8q6hz7exmwhsdqg972a028nwspp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqrhfnrc" -> PublicKey(hex"02e813a1f2f6e2066fa989d4daba5d48a88d02d6ab81f0e271d919691961550c0f"),
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"),
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"),
- "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qrjgq7md5lu2hhkz657rs2a40xm2elaqda4krv6vy44my49x02azsqwr35puvgzjltd6dfth2awxcq49cx3srkl3zl34xhw7ppv840yf74wqq88rwr5" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
- "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qr3gqjdynggx20rz4nh98uknmtp2wkwk95zru8lfmw0cz9s3t0xpevuzpzz4k34cprpg9jfc3yp8zc827psug69j4w4pkn70rrfddcqf9wnqqcm2nc4" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
"lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9q9sqsgqruuf6y6hd77533p6ufl3dapzzt55uj7t88mgty7hvfpy5lzvntpyn82j72fr3wqz985lh7l2f5pnju66nman5z09p24qvp2k8443skqqq38n4w" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
"lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qxpqqsgqh88td9f8p8ls8r6devh9lhvppwqe6e0lkvehyu8ztu76m9s8nu2x0rfp5z9jmn2ta97mex2ne6yecvtz8r0qej62lvkngpaduhgytncqts4cxs" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
"lnbc600n1p547aawpp55m20ku0uua76kmjw0dn54sajhzv5czg357vu4v8sd6mtylsydn9qcqpjsp5yhq5n0qthcw35ngp65zgcv4qswg9j96tq74jahyqv0wm6g3qptaq9qrsgqlqqdqlv93kxmm4de6xzcnvv5sxjmnkda5kxeg939n82lq9kcjzpxjv3d8ull0u5fe8v389sjq9457yljmfuml33wruy0wcl78vp6hqtrvpl7kg2wsga2zdfwx29937hecvdnu7wzpnrgqre2xnf" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"),
)
-
for ((req, nodeId) <- requests) {
val Success(invoice) = Bolt11Invoice.fromString(req)
assert(invoice.nodeId == nodeId)
@@ -669,26 +664,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
}
test("no unknown feature in invoice") {
- val invoiceFeatures = TestConstants.Alice.nodeParams.features.bolt11Features()
- assert(invoiceFeatures.unknown.nonEmpty)
+ val invoiceFeatures = TestConstants.Alice.nodeParams.features.bolt11Features().copy(encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(1105))))
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = invoiceFeatures)
assert(invoice.features == Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))
assert(Bolt11Invoice.fromString(invoice.toString).get == invoice)
}
- test("filter non-invoice features when parsing invoices") {
- // The following invoice has feature bit 20 activated (option_anchor_outputs) without feature bit 12 (option_static_remotekey).
- // This doesn't satisfy the feature dependency graph, but since those aren't invoice features, we should ignore it.
- val features = Features(
- Map(VariableLengthOnion -> FeatureSupport.Mandatory, PaymentSecret -> FeatureSupport.Mandatory, AnchorOutputs -> Mandatory),
- Set(UnknownFeature(121), UnknownFeature(156))
- )
- val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, None, randomBytes32(), priv, Left("non-invoice features"), CltvExpiryDelta(6), features = features.unscoped()).toString
- val Success(pr) = Bolt11Invoice.fromString(invoice)
- assert(pr.features == features.remove(AnchorOutputs))
- }
-
test("invoices can't have high features") {
- assertThrows[Exception](createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features[Feature](Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(424242)))))
+ assertThrows[Exception](createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features[Feature](Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Some(EncodedFeatures.fromFeatureBits(Set(424242))))))
}
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala
index b0c7dd4..731c075 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala
@@ -38,7 +38,7 @@ import fr.acinq.eclair.router.BlindedRouteCreation
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer}
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, Features, MilliSatoshiLong, NodeParams, PaymentFinalExpiryConf, TestConstants, TestKitBaseClass, TimestampMilliLong, TimestampSecond, UnknownFeature, randomBytes32, randomKey}
+import fr.acinq.eclair.{Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedFeatures, EncodedNodeId, Feature, Features, MilliSatoshiLong, NodeParams, PaymentFinalExpiryConf, TestConstants, TestKitBaseClass, TimestampMilliLong, TimestampSecond, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -146,7 +146,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
import f._
val testCases: Seq[Features[Feature]] = Seq(
Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory),
- Features(Map(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), unknown = Set(UnknownFeature(42))),
+ Features(Map(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), encoded_opt = Some(EncodedFeatures.fromFeatureBits(Set(42)))),
)
testCases.foreach { invoiceFeatures =>
val taggedFields = List(
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
index 970f5d2..baaaf8c 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
@@ -625,13 +625,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
ChannelReestablish(randomBytes32(), 242842L, 42L, randomKey(), randomKey().publicKey),
UnknownMessage(tag = 60000, data = ByteVector32.One.bytes),
)
-
- msgs.foreach {
- msg => {
- val encoded = lightningMessageCodecWithFallback.encode(msg).require
- val decoded = lightningMessageCodecWithFallback.decode(encoded).require
- assert(msg == decoded.value)
- }
+ msgs.foreach { msg =>
+ val encoded = lightningMessageCodecWithFallback.encode(msg).require
+ val decoded = lightningMessageCodecWithFallback.decode(encoded).require
+ assert(msg == decoded.value)
}
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala
index 7bc7ef1..9f8a5a3 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala
@@ -25,7 +25,7 @@ import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedHop, BlindedRoute}
import fr.acinq.eclair.wire.protocol.CommonCodecs.varintoverflow
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, offerTlvCodec}
import fr.acinq.eclair.wire.protocol.OfferTypes._
-import fr.acinq.eclair.{BlockHeight, EncodedNodeId, Features, MilliSatoshiLong, RealShortChannelId, randomBytes32, randomKey}
+import fr.acinq.eclair.{BlockHeight, EncodedNodeId, FeatureSupport, Features, MilliSatoshiLong, RealShortChannelId, randomBytes32, randomKey}
import org.json4s.DefaultFormats
import org.json4s.jackson.JsonMethods
import org.scalatest.funsuite.AnyFunSuite
@@ -36,8 +36,8 @@ import java.io.File
import scala.io.Source
class OfferTypesSpec extends AnyFunSuite {
- val nodeKey = PrivateKey(hex"85d08273493e489b9330c85a3e54123874c8cd67c1bf531f4b926c9c555f8e1d")
- val nodeId = nodeKey.publicKey
+ private val nodeKey = PrivateKey(hex"85d08273493e489b9330c85a3e54123874c8cd67c1bf531f4b926c9c555f8e1d")
+ private val nodeId = nodeKey.publicKey
test("invoice request is signed") {
val sellerKey = randomKey()
@@ -315,9 +315,11 @@ class OfferTypesSpec extends AnyFunSuite {
val src = Source.fromFile(new File(getClass.getResource(s"/offers-test.json").getFile))
val testVectors = JsonMethods.parse(src.mkString).extract[Seq[TestVector]]
src.close()
+ val allFeatures = Features(Features.knownFeatures.map(f => f -> FeatureSupport.Optional).toMap).bolt12Features()
for (vector <- testVectors) {
val offer = Offer.decode(vector.bolt12)
- assert((offer.isSuccess && offer.get.features.unknown.forall(_.bitIndex % 2 == 1)) == vector.valid, vector.description)
+ val featuresOk = offer.map(o => allFeatures.areSupported(o.features)).getOrElse(true)
+ assert((offer.isSuccess && featuresOk) == vector.valid, vector.description)
}
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala
index f48d508..081c588 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala
@@ -6,7 +6,7 @@ import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRouteDetails
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.{RouteBlindingDecryptedData, blindedRouteDataCodec}
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv._
-import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshiLong, ShortChannelId, UInt64, UnknownFeature, randomKey}
+import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, EncodedFeatures, Feature, FeatureSupport, Features, MilliSatoshiLong, ShortChannelId, UInt64, randomKey}
import org.scalatest.funsuite.AnyFunSuiteLike
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -18,7 +18,7 @@ class RouteBlindingSpec extends AnyFunSuiteLike {
hex"011a0000000000000000000000000000000000000000000000000000 020800000000000006c1 0a080024000000962710 0c06000b69e505dc 0e00 fd023103123456" -> TlvStream(Set[RouteBlindingEncryptedDataTlv](Padding(hex"0000000000000000000000000000000000000000000000000000"), OutgoingChannelId(ShortChannelId(1729)), PaymentRelay(CltvExpiryDelta(36), 150, 10000 msat), PaymentConstraints(CltvExpiry(748005), 1500 msat), AllowedFeatures(Features.empty)), Set(GenericTlv(UInt64(561), hex"123456"))),
hex"02080000000000000451 0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 0a0800300000006401f4 0c06000b69c105dc 0e00" -> TlvStream(OutgoingChannelId(ShortChannelId(1105)), NextPathKey(PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")), PaymentRelay(CltvExpiryDelta(48), 100, 500 msat), PaymentConstraints(CltvExpiry(747969), 1500 msat), AllowedFeatures(Features.empty)),
hex"01230000000000000000000000000000000000000000000000000000000000000000000000 02080000000000000231 0a060090000000fa 0c06000b699105dc 0e00" -> TlvStream(Padding(hex"0000000000000000000000000000000000000000000000000000000000000000000000"), OutgoingChannelId(ShortChannelId(561)), PaymentRelay(CltvExpiryDelta(144), 250, 0 msat), PaymentConstraints(CltvExpiry(747921), 1500 msat), AllowedFeatures(Features.empty)),
- hex"011a0000000000000000000000000000000000000000000000000000 0604deadbeef 0c06000b690105dc 0e0f020000000000000000000000000000 fdffff0206c1" -> TlvStream(Set[RouteBlindingEncryptedDataTlv](Padding(hex"0000000000000000000000000000000000000000000000000000"), PathId(hex"deadbeef"), PaymentConstraints(CltvExpiry(747777), 1500 msat), AllowedFeatures(Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(113))))), Set(GenericTlv(UInt64(65535), hex"06c1"))),
+ hex"011a0000000000000000000000000000000000000000000000000000 0604deadbeef 0c06000b690105dc 0e0f020000000000000000000000000000 fdffff0206c1" -> TlvStream(Set[RouteBlindingEncryptedDataTlv](Padding(hex"0000000000000000000000000000000000000000000000000000"), PathId(hex"deadbeef"), PaymentConstraints(CltvExpiry(747777), 1500 msat), AllowedFeatures(Features(Map.empty[Feature, FeatureSupport], Some(EncodedFeatures.fromFeatureBits(Set(113)))))), Set(GenericTlv(UInt64(65535), hex"06c1"))),
// Onion message reference test vector.
hex"01080000000000000000 042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145" -> TlvStream(Padding(hex"0000000000000000"), OutgoingNodeId(PublicKey(hex"02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"))),
hex"0109000000000000000000 06204242424242424242424242424242424242424242424242424242424242424242" -> TlvStream(Padding(hex"000000000000000000"), PathId(hex"4242424242424242424242424242424242424242424242424242424242424242")),
diff --git a/eclair-node/src/test/resources/api/getinfo b/eclair-node/src/test/resources/api/getinfo
index c6a8231..d07698c 100644
--- a/eclair-node/src/test/resources/api/getinfo
+++ b/eclair-node/src/test/resources/api/getinfo
@@ -1 +1 @@
-{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries_ex":"optional"},"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":9999,"publicAddresses":["127.0.0.1:9731"],"instanceId":"01234567-0123-4567-89ab-0123456789ab"}
\ No newline at end of file
+{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries_ex":"optional"}},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":9999,"publicAddresses":["127.0.0.1:9731"],"instanceId":"01234567-0123-4567-89ab-0123456789ab"}
\ No newline at end of file
diff --git a/eclair-node/src/test/resources/api/received-expired b/eclair-node/src/test/resources/api/received-expired
index b7859e7..33960f0 100644
--- a/eclair-node/src/test/resources/api/received-expired
+++ b/eclair-node/src/test/resources/api/received-expired
@@ -1 +1 @@
-{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03b20720d18c5f4c9c90c281fc511993556abc8c35a1a7c0f050a24b332c1cc105","serialized":"lnbc2500u1pvjluezsp5ggx65m9the9g2fjxhkhmguafsjv832charwnrpqtr4gwq3lae9sqpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspv9fxh2","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}}
\ No newline at end of file
+{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03b20720d18c5f4c9c90c281fc511993556abc8c35a1a7c0f050a24b332c1cc105","serialized":"lnbc2500u1pvjluezsp5ggx65m9the9g2fjxhkhmguafsjv832charwnrpqtr4gwq3lae9sqpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspv9fxh2","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{}},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}}
\ No newline at end of file
diff --git a/eclair-node/src/test/resources/api/received-pending b/eclair-node/src/test/resources/api/received-pending
index 34aa8a3..e334d24 100644
--- a/eclair-node/src/test/resources/api/received-pending
+++ b/eclair-node/src/test/resources/api/received-pending
@@ -1 +1 @@
-{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e2d79e66bc01619697188e9847e463c70b35e49e0e86b09dcb4b124925c4c550","serialized":"lnbc2500u1pvjluezsp5p0yyewlxryakfx0pt6fu2j7ct2jhgaw7hd62nqwm9yvh2cp8eurspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rsp9dda9d","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}}
\ No newline at end of file
+{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e2d79e66bc01619697188e9847e463c70b35e49e0e86b09dcb4b124925c4c550","serialized":"lnbc2500u1pvjluezsp5p0yyewlxryakfx0pt6fu2j7ct2jhgaw7hd62nqwm9yvh2cp8eurspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rsp9dda9d","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{}},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}}
\ No newline at end of file
diff --git a/eclair-node/src/test/resources/api/received-success b/eclair-node/src/test/resources/api/received-success
index c702992..cccda0a 100644
--- a/eclair-node/src/test/resources/api/received-success
+++ b/eclair-node/src/test/resources/api/received-success
@@ -1 +1 @@
-{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03779dc8b593b74509fab7c8accebc7a9b91d85d9df456d5b885464a34e5751d52","serialized":"lnbc2500u1pvjluezsp5cssgls5lpvunj7zallxsn3v8g3f9wqfs75hsdmkrtxwgkafers0spp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspzma2k0","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}}
\ No newline at end of file
+{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03779dc8b593b74509fab7c8accebc7a9b91d85d9df456d5b885464a34e5751d52","serialized":"lnbc2500u1pvjluezsp5cssgls5lpvunj7zallxsn3v8g3f9wqfs75hsdmkrtxwgkafers0spp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspzma2k0","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{}},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}}
\ No newline at end of file
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.