1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 22:46:44 +01:00

Replace FeatureScope by type hierarchy (#2207)

Instead of defining a separate type `FeatureScope` with its own
hierarchy, that was then mixed in `Feature` using the cake pattern, we
go with a simpler type hierarchy for `Feature`.

This significantly simplifies type declarations (no more `Feature with
FeatureScope`) especially in the tests.
This commit is contained in:
Pierre-Marie Padiou 2022-03-15 16:04:20 +01:00 committed by GitHub
parent f14300e33f
commit 7b5cefaf99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 118 additions and 119 deletions

View file

@ -60,7 +60,7 @@ import scala.collection.immutable.SortedMap
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise} import scala.concurrent.{ExecutionContext, Future, Promise}
case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features[FeatureScope], chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String) case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features[Feature], chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String)
case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed])

View file

@ -34,10 +34,9 @@ object FeatureSupport {
case object Optional extends FeatureSupport { override def toString: String = "optional" } case object Optional extends FeatureSupport { override def toString: String = "optional" }
} }
/** Not a sealed trait, so it can be extended by plugins. */
trait Feature { trait Feature {
this: FeatureScope =>
def rfcName: String def rfcName: String
def mandatory: Int def mandatory: Int
def optional: Int = mandatory + 1 def optional: Int = mandatory + 1
@ -48,24 +47,22 @@ trait Feature {
} }
override def toString = rfcName override def toString = rfcName
} }
/** Feature scope as defined in Bolt 9. */ /** Feature scope as defined in Bolt 9. */
sealed trait FeatureScope
/** Feature that should be advertised in init messages. */ /** Feature that should be advertised in init messages. */
trait InitFeature extends FeatureScope trait InitFeature extends Feature
/** Feature that should be advertised in node announcements. */ /** Feature that should be advertised in node announcements. */
trait NodeFeature extends FeatureScope trait NodeFeature extends Feature
/** Feature that should be advertised in invoices. */ /** Feature that should be advertised in invoices. */
trait InvoiceFeature extends FeatureScope trait InvoiceFeature extends Feature
// @formatter:on // @formatter:on
case class UnknownFeature(bitIndex: Int) case class UnknownFeature(bitIndex: Int)
case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) {
def hasFeature(feature: Feature with T, support: Option[FeatureSupport] = None): Boolean = support match { def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match {
case Some(s) => activated.get(feature).contains(s) case Some(s) => activated.get(feature).contains(s)
case None => activated.contains(feature) case None => activated.contains(feature)
} }
@ -84,13 +81,13 @@ case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSup
unknownFeaturesOk && knownFeaturesOk unknownFeaturesOk && knownFeaturesOk
} }
def initFeatures(): Features[InitFeature] = Features[InitFeature](activated.collect { case (f: InitFeature, s) => (f, s) }, unknown) def initFeatures(): Features[InitFeature] = Features(activated.collect { case (f: InitFeature, s) => (f, s) }, unknown)
def nodeAnnouncementFeatures(): Features[NodeFeature] = Features[NodeFeature](activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown) def nodeAnnouncementFeatures(): Features[NodeFeature] = Features(activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown)
def invoiceFeatures(): Features[InvoiceFeature] = Features[InvoiceFeature](activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown) def invoiceFeatures(): Features[InvoiceFeature] = Features(activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown)
def unscoped(): Features[FeatureScope] = Features[FeatureScope](activated.collect { case (f, s) => (f: Feature with FeatureScope, s) }, unknown) def unscoped(): Features[Feature] = Features[Feature](activated.collect { case (f, s) => (f: Feature, s) }, unknown)
def toByteVector: ByteVector = { def toByteVector: ByteVector = {
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet) val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet)
@ -116,28 +113,28 @@ case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSup
object Features { object Features {
def empty[T <: FeatureScope]: Features[T] = Features[T](Map.empty[Feature with T, FeatureSupport]) def empty[T <: Feature]: Features[T] = Features[T](Map.empty[T, FeatureSupport])
def apply[T <: FeatureScope](features: (Feature with T, FeatureSupport)*): Features[T] = Features[T](Map.from(features)) def apply[T <: Feature](features: (T, FeatureSupport)*): Features[T] = Features[T](Map.from(features))
def apply(bytes: ByteVector): Features[FeatureScope] = apply(bytes.bits) def apply(bytes: ByteVector): Features[Feature] = apply(bytes.bits)
def apply(bits: BitVector): Features[FeatureScope] = { def apply(bits: BitVector): Features[Feature] = {
val all = bits.toIndexedSeq.reverse.zipWithIndex.collect { 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(_.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) if knownFeatures.exists(_.mandatory == idx) => Right((knownFeatures.find(_.mandatory == idx).get, Mandatory))
case (true, idx) => Left(UnknownFeature(idx)) case (true, idx) => Left(UnknownFeature(idx))
} }
Features[FeatureScope]( Features[Feature](
activated = all.collect { case Right((feature, support)) => feature -> support }.toMap, activated = all.collect { case Right((feature, support)) => feature -> support }.toMap,
unknown = all.collect { case Left(inf) => inf }.toSet unknown = all.collect { case Left(inf) => inf }.toSet
) )
} }
def fromConfiguration[T <: FeatureScope](config: Config, validFeatures: Set[Feature with T]): Features[T] = Features[T]( def fromConfiguration[T <: Feature](config: Config, validFeatures: Set[T]): Features[T] = Features[T](
config.root().entrySet().asScala.flatMap { entry => config.root().entrySet().asScala.flatMap { entry =>
val featureName = entry.getKey val featureName = entry.getKey
val feature: Feature with T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)")) val feature: T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)"))
config.getString(featureName) match { config.getString(featureName) match {
case support if support == Mandatory.toString => Some(feature -> Mandatory) case support if support == Mandatory.toString => Some(feature -> Mandatory)
case support if support == Optional.toString => Some(feature -> Optional) case support if support == Optional.toString => Some(feature -> Optional)
@ -146,7 +143,7 @@ object Features {
} }
}.toMap) }.toMap)
def fromConfiguration(config: Config): Features[FeatureScope] = fromConfiguration[FeatureScope](config, knownFeatures) def fromConfiguration(config: Config): Features[Feature] = fromConfiguration[Feature](config, knownFeatures)
case object DataLossProtect extends Feature with InitFeature with NodeFeature { case object DataLossProtect extends Feature with InitFeature with NodeFeature {
val rfcName = "option_data_loss_protect" val rfcName = "option_data_loss_protect"
@ -242,7 +239,7 @@ object Features {
val mandatory = 54 val mandatory = 54
} }
val knownFeatures: Set[Feature with FeatureScope] = Set( val knownFeatures: Set[Feature] = Set(
DataLossProtect, DataLossProtect,
InitialRoutingSync, InitialRoutingSync,
UpfrontShutdownScript, UpfrontShutdownScript,
@ -278,16 +275,16 @@ object Features {
case class FeatureException(message: String) extends IllegalArgumentException(message) case class FeatureException(message: String) extends IllegalArgumentException(message)
def validateFeatureGraph[T <: FeatureScope](features: Features[T]): Option[FeatureException] = featuresDependency.collectFirst { def validateFeatureGraph[T <: Feature](features: Features[T]): Option[FeatureException] = featuresDependency.collectFirst {
case (feature, dependencies) if features.unscoped().hasFeature(feature) && dependencies.exists(d => !features.unscoped().hasFeature(d)) => case (feature, dependencies) if features.unscoped().hasFeature(feature) && dependencies.exists(d => !features.unscoped().hasFeature(d)) =>
FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.unscoped().hasFeature(d)).mkString(" and ")})") FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.unscoped().hasFeature(d)).mkString(" and ")})")
} }
/** Returns true if both feature sets are compatible. */ /** Returns true if both feature sets are compatible. */
def areCompatible[T <: FeatureScope](ours: Features[T], theirs: Features[T]): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours) def areCompatible[T <: Feature](ours: Features[T], theirs: Features[T]): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours)
/** returns true if both have at least optional support */ /** returns true if both have at least optional support */
def canUseFeature[T <: FeatureScope](localFeatures: Features[T], remoteFeatures: Features[T], feature: Feature with T): Boolean = { def canUseFeature[T <: Feature](localFeatures: Features[T], remoteFeatures: Features[T], feature: T): Boolean = {
localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature) localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature)
} }

View file

@ -58,7 +58,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
color: Color, color: Color,
publicAddresses: List[NodeAddress], publicAddresses: List[NodeAddress],
torAddress_opt: Option[NodeAddress], torAddress_opt: Option[NodeAddress],
features: Features[FeatureScope], features: Features[Feature],
private val overrideInitFeatures: Map[PublicKey, Features[InitFeature]], private val overrideInitFeatures: Map[PublicKey, Features[InitFeature]],
syncWhitelist: Set[PublicKey], syncWhitelist: Set[PublicKey],
pluginParams: Seq[PluginParams], pluginParams: Seq[PluginParams],
@ -271,7 +271,7 @@ object NodeParams extends Logging {
val nodeAlias = config.getString("node-alias") val nodeAlias = config.getString("node-alias")
require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)")
def validateFeatures(features: Features[FeatureScope]): Unit = { def validateFeatures(features: Features[Feature]): Unit = {
val featuresErr = Features.validateFeatureGraph(features) val featuresErr = Features.validateFeatureGraph(features)
require(featuresErr.isEmpty, featuresErr.map(_.message)) require(featuresErr.isEmpty, featuresErr.map(_.message))
require(features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)), s"${Features.VariableLengthOnion.rfcName} must be enabled and mandatory") require(features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)), s"${Features.VariableLengthOnion.rfcName} must be enabled and mandatory")
@ -291,11 +291,11 @@ object NodeParams extends Logging {
require(Features.knownFeatures.map(_.mandatory).intersect(pluginFeatureSet).isEmpty, "Plugin feature bit overlaps with known feature bit") 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(pluginFeatureSet.size == pluginMessageParams.size, "Duplicate plugin feature bits found")
val coreAndPluginFeatures: Features[FeatureScope] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature)) val coreAndPluginFeatures: Features[Feature] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature))
val overrideInitFeatures: Map[PublicKey, Features[InitFeature]] = config.getConfigList("override-init-features").asScala.map { e => val overrideInitFeatures: Map[PublicKey, Features[InitFeature]] = config.getConfigList("override-init-features").asScala.map { e =>
val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid")))
val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: Feature with InitFeature => f }) val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: InitFeature => f })
validateFeatures(f.unscoped()) validateFeatures(f.unscoped())
p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature]) p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature])
}.toMap }.toMap

View file

@ -17,7 +17,7 @@
package fr.acinq.eclair.channel package fr.acinq.eclair.channel
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
import fr.acinq.eclair.{Feature, FeatureScope, FeatureSupport, Features, InitFeature} import fr.acinq.eclair.{FeatureSupport, Features, InitFeature, Feature}
/** /**
* Created by t-bast on 24/06/2021. * Created by t-bast on 24/06/2021.
@ -28,7 +28,7 @@ import fr.acinq.eclair.{Feature, FeatureScope, FeatureSupport, Features, InitFea
* Even if one of these features is later disabled at the connection level, it will still apply to the channel until the * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
* channel is upgraded or closed. * channel is upgraded or closed.
*/ */
case class ChannelFeatures(features: Set[Feature with FeatureScope]) { case class ChannelFeatures(features: Set[Feature]) {
val channelType: SupportedChannelType = { val channelType: SupportedChannelType = {
if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) {
@ -45,7 +45,7 @@ case class ChannelFeatures(features: Set[Feature with FeatureScope]) {
val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet
val commitmentFormat: CommitmentFormat = channelType.commitmentFormat val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
def hasFeature(feature: Feature with FeatureScope): Boolean = features.contains(feature) def hasFeature(feature: Feature): Boolean = features.contains(feature)
override def toString: String = features.mkString(",") override def toString: String = features.mkString(",")
@ -53,13 +53,13 @@ case class ChannelFeatures(features: Set[Feature with FeatureScope]) {
object ChannelFeatures { object ChannelFeatures {
def apply(features: (Feature with FeatureScope)*): ChannelFeatures = ChannelFeatures(Set.from(features)) def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features))
/** Enrich the channel type with other permanent features that will be applied to the channel. */ /** Enrich the channel type with other permanent features that will be applied to the channel. */
def apply(channelType: ChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = { def apply(channelType: ChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = {
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation, // NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
// such as option_dataloss_protect or option_shutdown_anysegwit. // such as option_dataloss_protect or option_shutdown_anysegwit.
val availableFeatures: Seq[Feature with FeatureScope] = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) val availableFeatures = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
val allFeatures = channelType.features.toSeq ++ availableFeatures val allFeatures = channelType.features.toSeq ++ availableFeatures
ChannelFeatures(allFeatures: _*) ChannelFeatures(allFeatures: _*)
} }
@ -69,7 +69,7 @@ object ChannelFeatures {
/** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */ /** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */
sealed trait ChannelType { sealed trait ChannelType {
/** Features representing that channel type. */ /** Features representing that channel type. */
def features: Set[Feature with InitFeature] def features: Set[InitFeature]
} }
sealed trait SupportedChannelType extends ChannelType { sealed trait SupportedChannelType extends ChannelType {
@ -84,31 +84,31 @@ object ChannelTypes {
// @formatter:off // @formatter:off
case object Standard extends SupportedChannelType { case object Standard extends SupportedChannelType {
override def features: Set[Feature with InitFeature] = Set.empty override def features: Set[InitFeature] = Set.empty
override def paysDirectlyToWallet: Boolean = false override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
override def toString: String = "standard" override def toString: String = "standard"
} }
case object StaticRemoteKey extends SupportedChannelType { case object StaticRemoteKey extends SupportedChannelType {
override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey) override def features: Set[InitFeature] = Set(Features.StaticRemoteKey)
override def paysDirectlyToWallet: Boolean = true override def paysDirectlyToWallet: Boolean = true
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
override def toString: String = "static_remotekey" override def toString: String = "static_remotekey"
} }
case object AnchorOutputs extends SupportedChannelType { case object AnchorOutputs extends SupportedChannelType {
override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) override def features: Set[InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs)
override def paysDirectlyToWallet: Boolean = false override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat
override def toString: String = "anchor_outputs" override def toString: String = "anchor_outputs"
} }
case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType {
override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) override def features: Set[InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)
override def paysDirectlyToWallet: Boolean = false override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
override def toString: String = "anchor_outputs_zero_fee_htlc_tx" override def toString: String = "anchor_outputs_zero_fee_htlc_tx"
} }
case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
override def features: Set[Feature with InitFeature] = featureBits.activated.keySet override def features: Set[InitFeature] = featureBits.activated.keySet
override def toString: String = s"0x${featureBits.toByteVector.toHex}" override def toString: String = s"0x${featureBits.toByteVector.toHex}"
} }
// @formatter:on // @formatter:on

View file

@ -225,7 +225,7 @@ object Helpers {
} }
def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = {
val features = Features.empty[FeatureScope] // empty features for now val features = Features.empty[Feature] // empty features for now
val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)
val witness = Announcements.generateChannelAnnouncementWitness( val witness = Announcements.generateChannelAnnouncementWitness(
nodeParams.chainHash, nodeParams.chainHash,

View file

@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureScope, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} import fr.acinq.eclair.{FSMDiagnosticActorLogging, Feature, Features, InitFeature, Logs, TimestampMilli, TimestampSecond}
import scodec.Attempt import scodec.Attempt
import scodec.bits.ByteVector import scodec.bits.ByteVector

View file

@ -35,7 +35,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc
import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Feature, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature}
import org.json4s import org.json4s
import org.json4s.JsonAST._ import org.json4s.JsonAST._
import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair.payment
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.bits.{BitVector, ByteOrdering, ByteVector}
import scodec.codecs.{list, ubyte} import scodec.codecs.{list, ubyte}
import scodec.{Codec, Err} import scodec.{Codec, Err}
@ -299,7 +299,7 @@ object Bolt11Invoice {
* This returns a bitvector with the minimum size necessary to encode the features, left padded to have a length (in * This returns a bitvector with the minimum size necessary to encode the features, left padded to have a length (in
* bits) that is a multiple of 5. * bits) that is a multiple of 5.
*/ */
def features2bits[T <: FeatureScope](features: Features[T]): BitVector = leftPaddedBits(features.toByteVector.bits) def features2bits[T <: Feature](features: Features[T]): BitVector = leftPaddedBits(features.toByteVector.bits)
private def leftPaddedBits(bits: BitVector): BitVector = { private def leftPaddedBits(bits: BitVector): BitVector = {
var highest = -1 var highest = -1
@ -365,7 +365,7 @@ object Bolt11Invoice {
/** /**
* Features supported or required for receiving this payment. * Features supported or required for receiving this payment.
*/ */
case class InvoiceFeatures(features: Features[FeatureScope]) extends TaggedField case class InvoiceFeatures(features: Features[Feature]) extends TaggedField
object Codecs { object Codecs {
@ -408,7 +408,7 @@ object Bolt11Invoice {
.typecase(2, dataCodec(bits).as[UnknownTag2]) .typecase(2, dataCodec(bits).as[UnknownTag2])
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo]) .typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo])
.typecase(4, dataCodec(bits).as[UnknownTag4]) .typecase(4, dataCodec(bits).as[UnknownTag4])
.typecase(5, dataCodec(bits).xmap[Features[FeatureScope]](Features(_), features2bits).as[InvoiceFeatures]) .typecase(5, dataCodec(bits).xmap[Features[Feature]](Features(_), features2bits).as[InvoiceFeatures])
.typecase(6, dataCodec(bits).as[Expiry]) .typecase(6, dataCodec(bits).as[Expiry])
.typecase(7, dataCodec(bits).as[UnknownTag7]) .typecase(7, dataCodec(bits).as[UnknownTag7])
.typecase(8, dataCodec(bits).as[UnknownTag8]) .typecase(8, dataCodec(bits).as[UnknownTag8])

View file

@ -31,7 +31,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.queryFlagsCodec import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.queryFlagsCodec
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, InitFeature} import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InitFeature}
import scodec._ import scodec._
import scodec.codecs._ import scodec.codecs._
@ -98,7 +98,7 @@ object EclairInternalsSerializer {
("channelQueryChunkSize" | int32) :: ("channelQueryChunkSize" | int32) ::
("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf] ("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf]
val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[FeatureScope])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[Feature])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec))
val peerConnectionConfCodec: Codec[PeerConnection.Conf] = ( val peerConnectionConfCodec: Codec[PeerConnection.Conf] = (
("authTimeout" | finiteDurationCodec) :: ("authTimeout" | finiteDurationCodec) ::

View file

@ -19,7 +19,7 @@ package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult}
import scodec.bits.ByteVector import scodec.bits.ByteVector
import shapeless.HNil import shapeless.HNil
@ -28,16 +28,16 @@ import shapeless.HNil
*/ */
object Announcements { object Announcements {
def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[FeatureScope], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[Feature], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector =
sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil)))) sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil))))
def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[FeatureScope], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[Feature], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector =
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: tlvStream :: HNil)))) sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: tlvStream :: HNil))))
def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector =
sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil)))) sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil))))
def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[FeatureScope]): ByteVector = def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[Feature]): ByteVector =
if (isNode1(localNodeId, remoteNodeId)) { if (isNode1(localNodeId, remoteNodeId)) {
channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty) channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty)
} else { } else {

View file

@ -23,7 +23,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.{BlockHeight, Feature, FeatureScope, Features, channel} import fr.acinq.eclair.{BlockHeight, Features, Feature, channel}
import scodec.bits.BitVector import scodec.bits.BitVector
private[channel] object ChannelTypes0 { private[channel] object ChannelTypes0 {
@ -196,8 +196,8 @@ private[channel] object ChannelTypes0 {
ChannelConfig() ChannelConfig()
} }
val isWumboChannel = commitInput.txOut.amount > Satoshi(16777215) val isWumboChannel = commitInput.txOut.amount > Satoshi(16777215)
val baseChannelFeatures: Set[Feature with FeatureScope] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty val baseChannelFeatures: Set[Feature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty
val commitmentFeatures: Set[Feature with FeatureScope] = if (channelVersion.hasAnchorOutputs) { val commitmentFeatures: Set[Feature] = if (channelVersion.hasAnchorOutputs) {
Set(Features.StaticRemoteKey, Features.AnchorOutputs) Set(Features.StaticRemoteKey, Features.AnchorOutputs)
} else if (channelVersion.hasStaticRemotekey) { } else if (channelVersion.hasStaticRemotekey) {
Set(Features.StaticRemoteKey) Set(Features.StaticRemoteKey)

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol
import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.{FeatureScope, Features, InitFeature, KamonExt, NodeFeature} import fr.acinq.eclair.{Feature, Features, InitFeature, KamonExt, NodeFeature}
import scodec.bits.{BitVector, ByteVector} import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._ import scodec.codecs._
import scodec.{Attempt, Codec} import scodec.{Attempt, Codec}
@ -29,7 +29,7 @@ import shapeless._
*/ */
object LightningMessageCodecs { object LightningMessageCodecs {
val featuresCodec: Codec[Features[FeatureScope]] = varsizebinarydata.xmap[Features[FeatureScope]]( val featuresCodec: Codec[Features[Feature]] = varsizebinarydata.xmap[Features[Feature]](
{ bytes => Features(bytes) }, { bytes => Features(bytes) },
{ features => features.toByteVector } { features => features.toByteVector }
) )

View file

@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.channel.{ChannelFlags, ChannelType}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, FeatureScope, Features, InitFeature, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, UInt64} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, UInt64}
import scodec.bits.ByteVector import scodec.bits.ByteVector
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
@ -200,7 +200,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64,
nodeSignature2: ByteVector64, nodeSignature2: ByteVector64,
bitcoinSignature1: ByteVector64, bitcoinSignature1: ByteVector64,
bitcoinSignature2: ByteVector64, bitcoinSignature2: ByteVector64,
features: Features[FeatureScope], features: Features[Feature],
chainHash: ByteVector32, chainHash: ByteVector32,
shortChannelId: ShortChannelId, shortChannelId: ShortChannelId,
nodeId1: PublicKey, nodeId1: PublicKey,
@ -263,7 +263,7 @@ case class Tor3(tor3: String, port: Int) extends OnionAddress { override def soc
// @formatter:on // @formatter:on
case class NodeAnnouncement(signature: ByteVector64, case class NodeAnnouncement(signature: ByteVector64,
features: Features[FeatureScope], features: Features[Feature],
timestamp: TimestampSecond, timestamp: TimestampSecond,
nodeId: PublicKey, nodeId: PublicKey,
rgbColor: Color, rgbColor: Color,

View file

@ -109,7 +109,7 @@ class FeaturesSpec extends AnyFunSuite {
} }
test("features compatibility") { test("features compatibility") {
case class TestCase(ours: Features[FeatureScope], theirs: Features[FeatureScope], oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean) case class TestCase(ours: Features[Feature], theirs: Features[Feature], oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean)
val testCases = Seq( val testCases = Seq(
// Empty features // Empty features
TestCase( TestCase(
@ -168,7 +168,7 @@ class FeaturesSpec extends AnyFunSuite {
// They have unknown optional features // They have unknown optional features
TestCase( TestCase(
Features(VariableLengthOnion -> Optional), Features(VariableLengthOnion -> Optional),
Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(141))), Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(141))),
oursSupportTheirs = true, oursSupportTheirs = true,
theirsSupportOurs = true, theirsSupportOurs = true,
compatible = true compatible = true
@ -176,7 +176,7 @@ class FeaturesSpec extends AnyFunSuite {
// They have unknown mandatory features // They have unknown mandatory features
TestCase( TestCase(
Features(VariableLengthOnion -> Optional), Features(VariableLengthOnion -> Optional),
Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(142))), Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(142))),
oursSupportTheirs = false, oursSupportTheirs = false,
theirsSupportOurs = true, theirsSupportOurs = true,
compatible = false compatible = false
@ -198,10 +198,10 @@ class FeaturesSpec extends AnyFunSuite {
compatible = false compatible = false
), ),
// nonreg testing of future features (needs to be updated with every new supported mandatory bit) // nonreg testing of future features (needs to be updated with every new supported mandatory bit)
TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
) )
for (testCase <- testCases) { for (testCase <- testCases) {
@ -212,20 +212,20 @@ class FeaturesSpec extends AnyFunSuite {
} }
test("filter features based on their usage") { test("filter features based on their usage") {
val features = Features[FeatureScope]( val features = Features(
Map[Feature with FeatureScope, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
) )
assert(features.initFeatures() === Features[InitFeature]( assert(features.initFeatures() === Features(
Map[Feature with InitFeature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
)) ))
assert(features.nodeAnnouncementFeatures() === Features[NodeFeature]( assert(features.nodeAnnouncementFeatures() === Features(
Map[Feature with NodeFeature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), Map(DataLossProtect -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
)) ))
assert(features.invoiceFeatures() === Features[InvoiceFeature]( assert(features.invoiceFeatures() === Features(
Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), Map(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
)) ))
} }
@ -235,8 +235,8 @@ class FeaturesSpec extends AnyFunSuite {
hex"" -> Features.empty, hex"" -> Features.empty,
hex"0100" -> Features(VariableLengthOnion -> Mandatory), hex"0100" -> Features(VariableLengthOnion -> Mandatory),
hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional), hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional),
hex"09004200" -> Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), hex"09004200" -> Features(Map(VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))),
hex"52000000" -> Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30)))
) )
for ((bin, features) <- testCases) { for ((bin, features) <- testCases) {

View file

@ -84,8 +84,8 @@ object TestConstants {
color = Color(1, 2, 3), color = Color(1, 2, 3),
publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil,
torAddress_opt = None, torAddress_opt = None,
features = Features[FeatureScope]( features = Features(
Map[Feature with FeatureScope, FeatureSupport]( Map(
DataLossProtect -> Optional, DataLossProtect -> Optional,
ChannelRangeQueries -> Optional, ChannelRangeQueries -> Optional,
ChannelRangeQueriesExtended -> Optional, ChannelRangeQueriesExtended -> Optional,
@ -94,7 +94,7 @@ object TestConstants {
BasicMultiPartPayment -> Optional, BasicMultiPartPayment -> Optional,
PaymentMetadata -> Optional, PaymentMetadata -> Optional,
), ),
Set(UnknownFeature(TestFeature.optional)) unknown = Set(UnknownFeature(TestFeature.optional))
), ),
pluginParams = List(pluginParams), pluginParams = List(pluginParams),
overrideInitFeatures = Map.empty, overrideInitFeatures = Map.empty,

View file

@ -20,7 +20,7 @@ import fr.acinq.eclair.FeatureSupport._
import fr.acinq.eclair.Features._ import fr.acinq.eclair.Features._
import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods
import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.{Features, InitFeature, TestKitBaseClass} import fr.acinq.eclair.{Features, InitFeature, NodeFeature, TestKitBaseClass}
import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.funsuite.AnyFunSuiteLike
class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods {
@ -80,29 +80,31 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha
} }
test("create channel type from features") { test("create channel type from features") {
case class TestCase(features: Features[InitFeature], expectedChannelType: ChannelType)
val validChannelTypes = Seq( val validChannelTypes = Seq(
Features.empty[InitFeature] -> ChannelTypes.Standard, TestCase(Features.empty[InitFeature], ChannelTypes.Standard),
Features[InitFeature](StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, TestCase(Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory), ChannelTypes.AnchorOutputs),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx, TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx),
) )
for ((features, expected) <- validChannelTypes) { for (testCase <- validChannelTypes) {
assert(ChannelTypes.fromFeatures(features) === expected) assert(ChannelTypes.fromFeatures(testCase.features) === testCase.expectedChannelType)
} }
val invalidChannelTypes = Seq( val invalidChannelTypes: Seq[Features[InitFeature]] = Seq(
Features[InitFeature](Wumbo -> Optional), Features(Wumbo -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional),
Features[InitFeature](StaticRemoteKey -> Mandatory, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional),
) )
for (features <- invalidChannelTypes) { for (features <- invalidChannelTypes) {
assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features)) assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features))

View file

@ -353,7 +353,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
} }
// They want to use a channel type we don't support yet. // They want to use a channel type we don't support yet.
{ {
val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features[InitFeature](Map[Feature with InitFeature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22))))))) val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map(StaticRemoteKey -> Mandatory), unknown = Set(UnknownFeature(22)))))))
peerConnection.send(peer, open) peerConnection.send(peer, open)
peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard")) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard"))
} }

View file

@ -21,7 +21,7 @@ import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble,
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
import fr.acinq.eclair.payment.Bolt11Invoice._ import fr.acinq.eclair.payment.Bolt11Invoice._
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, Feature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import scodec.DecodeResult import scodec.DecodeResult
import scodec.bits._ import scodec.bits._
@ -54,7 +54,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
timestamp: TimestampSecond = TimestampSecond.now(), timestamp: TimestampSecond = TimestampSecond.now(),
paymentSecret: ByteVector32 = randomBytes32(), paymentSecret: ByteVector32 = randomBytes32(),
paymentMetadata: Option[ByteVector] = None, paymentMetadata: Option[ByteVector] = None,
features: Features[FeatureScope] = defaultFeatures.unscoped()): Bolt11Invoice = { features: Features[Feature] = defaultFeatures.unscoped()): Bolt11Invoice = {
require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret")
val prefix = prefixes(chainHash) val prefix = prefixes(chainHash)
val tags = { val tags = {
@ -624,11 +624,11 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("no unknown feature in invoice"){ test("no unknown feature in invoice"){
assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty) assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty)
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures()) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures())
assert(invoice.features === Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))) assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))
assert(Bolt11Invoice.fromString(invoice.toString) === invoice) assert(Bolt11Invoice.fromString(invoice.toString) === invoice)
} }
test("Invoices can't have high features"){ 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[FeatureScope](Map[Feature with FeatureScope, 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), Set(UnknownFeature(424242)))))
} }
} }

View file

@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart
import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler}
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureScope, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey}
import org.scalatest.Outcome import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scodec.bits.HexStringSyntax import scodec.bits.HexStringSyntax
@ -46,18 +46,18 @@ import scala.concurrent.duration._
class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val featuresWithoutMpp = Features[FeatureScope]( val featuresWithoutMpp = Features[Feature](
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
) )
val featuresWithMpp = Features[FeatureScope]( val featuresWithMpp = Features[Feature](
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional BasicMultiPartPayment -> Optional
) )
val featuresWithKeySend = Features[FeatureScope]( val featuresWithKeySend = Features[Feature](
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
KeySend -> Optional KeySend -> Optional

View file

@ -37,10 +37,10 @@ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv}
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag} import org.scalatest.{Outcome, Tag}
import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax} import scodec.bits.{ByteVector, HexStringSyntax}
import java.util.UUID import java.util.UUID
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -53,18 +53,18 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe) case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe)
val featuresWithoutMpp: Features[InvoiceFeature] = Features[InvoiceFeature]( val featuresWithoutMpp: Features[InvoiceFeature] = Features(
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory PaymentSecret -> Mandatory
) )
val featuresWithMpp: Features[InvoiceFeature] = Features[InvoiceFeature]( val featuresWithMpp: Features[InvoiceFeature] = Features(
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional, BasicMultiPartPayment -> Optional,
) )
val featuresWithTrampoline: Features[InvoiceFeature] = Features[InvoiceFeature]( val featuresWithTrampoline: Features[InvoiceFeature] = Features(
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional, BasicMultiPartPayment -> Optional,
@ -128,7 +128,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
Bolt11Invoice.Description("Some invoice"), Bolt11Invoice.Description("Some invoice"),
Bolt11Invoice.PaymentSecret(randomBytes32()), Bolt11Invoice.PaymentSecret(randomBytes32()),
Bolt11Invoice.Expiry(3600), Bolt11Invoice.Expiry(3600),
Bolt11Invoice.InvoiceFeatures(Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(42))).unscoped()) Bolt11Invoice.InvoiceFeatures(Features(Map(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), unknown = Set(UnknownFeature(42))))
) )
val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty)
val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)

View file

@ -54,7 +54,7 @@ class AnnouncementsSpec extends AnyFunSuite {
} }
test("create valid signed node announcement") { test("create valid signed node announcement") {
val features = Features[FeatureScope]( val features = Features(
Features.DataLossProtect -> FeatureSupport.Optional, Features.DataLossProtect -> FeatureSupport.Optional,
Features.InitialRoutingSync -> FeatureSupport.Optional, Features.InitialRoutingSync -> FeatureSupport.Optional,
Features.ChannelRangeQueries -> FeatureSupport.Optional, Features.ChannelRangeQueries -> FeatureSupport.Optional,

View file

@ -271,7 +271,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil) val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil)
val revoke_and_ack = RevokeAndAck(randomBytes32(), scalar(0), point(1)) val revoke_and_ack = RevokeAndAck(randomBytes32(), scalar(0), point(1))
val channel_announcement = ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) val channel_announcement = ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey)
val node_announcement = NodeAnnouncement(randomBytes64(), Features[FeatureScope](DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) val node_announcement = NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil)
val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None)
val announcement_signatures = AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()) val announcement_signatures = AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64())
val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500)