1
0
mirror of https://github.com/ACINQ/eclair.git synced 2025-01-19 05:33:59 +01:00

Typed features (#2164)

Give different types to init features / node announcement features / invoice features.

We now accept more invoices (we would previously reject invoices if they required a feature that we don't support but that we know is irrelevant for invoices).
This commit is contained in:
Thomas HUET 2022-02-07 09:51:38 +01:00 committed by GitHub
parent 553727cb22
commit 9401592382
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 406 additions and 361 deletions

View File

@ -99,10 +99,10 @@ It's usually risky to activate non-default features or disable default features:
Eclair supports per-peer features. Suppose you are connected to Alice and Bob, you can use a different set of features with Alice than the one you use with Bob.
When experimenting with non-default features, we recommend using this to scope the peers you want to experiment with.
This is done with the `override-features` configuration parameter in your `eclair.conf`:
This is done with the `override-init-features` configuration parameter in your `eclair.conf`:
```conf
eclair.override-features = [
eclair.override-init-features = [
{
nodeId = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"
features {

View File

@ -60,7 +60,7 @@ eclair {
trampoline_payment = disabled
keysend = disabled
}
override-features = [ // optional per-node features
override-init-features = [ // optional per-node features
# {
# nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
# features { }

View File

@ -60,7 +60,7 @@ import scala.collection.immutable.SortedMap
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features, 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[FeatureScope], 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])

View File

@ -20,6 +20,8 @@ import com.typesafe.config.Config
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import scodec.bits.{BitVector, ByteVector}
import scala.jdk.CollectionConverters.CollectionHasAsScala
/**
* Created by PM on 13/02/2017.
*/
@ -61,9 +63,9 @@ trait InvoiceFeature extends FeatureScope
case class UnknownFeature(bitIndex: Int)
case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) {
case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) {
def hasFeature(feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match {
def hasFeature(feature: Feature with T, support: Option[FeatureSupport] = None): Boolean = support match {
case Some(s) => activated.get(feature).contains(s)
case None => activated.contains(feature)
}
@ -71,7 +73,7 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow
def hasPluginFeature(feature: UnknownFeature): Boolean = unknown.contains(feature)
/** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */
def areSupported(remoteFeatures: Features): Boolean = {
def areSupported(remoteFeatures: Features[T]): Boolean = {
// we allow unknown odd features (it's ok to be odd)
val unknownFeaturesOk = remoteFeatures.unknown.forall(_.bitIndex % 2 == 1)
// we verify that we activated every mandatory feature they require
@ -82,12 +84,13 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow
unknownFeaturesOk && knownFeaturesOk
}
def initFeatures(): Features = Features(activated.collect { case (f: InitFeature, s) => (f: Feature, s) }, unknown)
def initFeatures(): Features[InitFeature] = Features[InitFeature](activated.collect { case (f: InitFeature, s) => (f, s) }, unknown)
def nodeAnnouncementFeatures(): Features = Features(activated.collect { case (f: NodeFeature, s) => (f: Feature, s) }, unknown)
def nodeAnnouncementFeatures(): Features[NodeFeature] = Features[NodeFeature](activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown)
// NB: we don't include unknown features in invoices, which means plugins cannot inject invoice features.
def invoiceFeatures(): Features = Features(activated.collect { case (f: InvoiceFeature, s) => (f: Feature, s) })
def invoiceFeatures(): Features[InvoiceFeature] = Features[InvoiceFeature](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 toByteVector: ByteVector = {
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet)
@ -113,47 +116,37 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow
object Features {
def empty = Features(Map.empty[Feature, FeatureSupport])
def empty[T <: FeatureScope]: Features[T] = Features[T](Map.empty[Feature with T, FeatureSupport])
def apply(features: (Feature, FeatureSupport)*): Features = Features(Map.from(features))
def apply[T <: FeatureScope](features: (Feature with T, FeatureSupport)*): Features[T] = Features[T](Map.from(features))
def apply(bytes: ByteVector): Features = apply(bytes.bits)
def apply(bytes: ByteVector): Features[FeatureScope] = apply(bytes.bits)
def apply(bits: BitVector): Features = {
def apply(bits: BitVector): Features[FeatureScope] = {
val all = bits.toIndexedSeq.reverse.zipWithIndex.collect {
case (true, idx) if knownFeatures.exists(_.optional == idx) => Right((knownFeatures.find(_.optional == idx).get, Optional))
case (true, idx) if knownFeatures.exists(_.mandatory == idx) => Right((knownFeatures.find(_.mandatory == idx).get, Mandatory))
case (true, idx) => Left(UnknownFeature(idx))
}
Features(
Features[FeatureScope](
activated = all.collect { case Right((feature, support)) => feature -> support }.toMap,
unknown = all.collect { case Left(inf) => inf }.toSet
)
}
/** expects to have a top level config block named "features" */
def fromConfiguration(config: Config): Features = Features(
knownFeatures.flatMap {
feature =>
getFeature(config, feature.rfcName) match {
case Some(support) => Some(feature -> support)
case _ => None
}
}.toMap)
/** tries to extract the given feature name from the config, if successful returns its feature support */
private def getFeature(config: Config, name: String): Option[FeatureSupport] = {
if (!config.hasPath(s"features.$name")) {
None
} else {
config.getString(s"features.$name") match {
case support if support == Mandatory.toString => Some(Mandatory)
case support if support == Optional.toString => Some(Optional)
def fromConfiguration[T <: FeatureScope](config: Config, validFeatures: Set[Feature with T]): Features[T] = Features[T](
config.root().entrySet().asScala.flatMap { entry =>
val featureName = entry.getKey
val feature: Feature with T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)"))
config.getString(featureName) match {
case support if support == Mandatory.toString => Some(feature -> Mandatory)
case support if support == Optional.toString => Some(feature -> Optional)
case support if support == "disabled" => None
case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)")
}
}
}
}.toMap)
def fromConfiguration(config: Config): Features[FeatureScope] = fromConfiguration[FeatureScope](config, knownFeatures)
case object DataLossProtect extends Feature with InitFeature with NodeFeature {
val rfcName = "option_data_loss_protect"
@ -249,7 +242,7 @@ object Features {
val mandatory = 54
}
val knownFeatures: Set[Feature] = Set(
val knownFeatures: Set[Feature with FeatureScope] = Set(
DataLossProtect,
InitialRoutingSync,
UpfrontShutdownScript,
@ -285,16 +278,16 @@ object Features {
case class FeatureException(message: String) extends IllegalArgumentException(message)
def validateFeatureGraph(features: Features): Option[FeatureException] = featuresDependency.collectFirst {
case (feature, dependencies) if features.hasFeature(feature) && dependencies.exists(d => !features.hasFeature(d)) =>
FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.hasFeature(d)).mkString(" and ")})")
def validateFeatureGraph[T <: FeatureScope](features: Features[T]): Option[FeatureException] = featuresDependency.collectFirst {
case (feature, dependencies) if features.unscoped().hasFeature(feature) && dependencies.exists(d => !features.unscoped().hasFeature(d)) =>
FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.unscoped().hasFeature(d)).mkString(" and ")})")
}
/** Returns true if both feature sets are compatible. */
def areCompatible(ours: Features, theirs: Features): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours)
def areCompatible[T <: FeatureScope](ours: Features[T], theirs: Features[T]): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours)
/** returns true if both have at least optional support */
def canUseFeature(localFeatures: Features, remoteFeatures: Features, feature: Feature): Boolean = {
def canUseFeature[T <: FeatureScope](localFeatures: Features[T], remoteFeatures: Features[T], feature: Feature with T): Boolean = {
localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature)
}

View File

@ -58,8 +58,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
color: Color,
publicAddresses: List[NodeAddress],
torAddress_opt: Option[NodeAddress],
features: Features,
private val overrideFeatures: Map[PublicKey, Features],
features: Features[FeatureScope],
private val overrideInitFeatures: Map[PublicKey, Features[InitFeature]],
syncWhitelist: Set[PublicKey],
pluginParams: Seq[PluginParams],
channelConf: ChannelConf,
@ -91,7 +91,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get)
/** Returns the features that should be used in our init message with the given peer. */
def initFeaturesFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features).initFeatures()
def initFeaturesFor(nodeId: PublicKey): Features[InitFeature] = overrideInitFeatures.getOrElse(nodeId, features).initFeatures()
}
object NodeParams extends Logging {
@ -224,6 +224,7 @@ object NodeParams extends Logging {
"watch-spent-window" -> "router.watch-spent-window",
// v0.7.1
"payment-request-expiry" -> "invoice-expiry",
"override-features" -> "override-init-features",
)
deprecatedKeyPaths.foreach {
case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'")
@ -268,7 +269,7 @@ object NodeParams extends Logging {
val nodeAlias = config.getString("node-alias")
require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)")
def validateFeatures(features: Features): Unit = {
def validateFeatures(features: Features[FeatureScope]): Unit = {
val featuresErr = Features.validateFeatureGraph(features)
require(featuresErr.isEmpty, featuresErr.map(_.message))
require(features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)), s"${Features.VariableLengthOnion.rfcName} must be enabled and mandatory")
@ -278,7 +279,7 @@ object NodeParams extends Logging {
}
val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p }
val features = Features.fromConfiguration(config)
val features = Features.fromConfiguration(config.getConfig("features"))
validateFeatures(features)
require(pluginMessageParams.forall(_.feature.mandatory > 128), "Plugin mandatory feature bit is too low, must be > 128")
@ -288,13 +289,13 @@ object NodeParams extends Logging {
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")
val coreAndPluginFeatures = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature))
val coreAndPluginFeatures: Features[FeatureScope] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature))
val overrideFeatures: Map[PublicKey, Features] = config.getConfigList("override-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 f = Features.fromConfiguration(e)
validateFeatures(f)
p -> f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature))
val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: Feature with InitFeature => f })
validateFeatures(f.unscoped())
p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature])
}.toMap
val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").asScala.map(s => PublicKey(ByteVector.fromValidHex(s))).toSet
@ -398,7 +399,7 @@ object NodeParams extends Logging {
torAddress_opt = torAddress_opt,
features = coreAndPluginFeatures,
pluginParams = pluginParams,
overrideFeatures = overrideFeatures,
overrideInitFeatures = overrideInitFeatures,
syncWhitelist = syncWhitelist,
channelConf = ChannelConf(
channelFlags = channelFlags,

View File

@ -24,7 +24,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, ShortChannelId, UInt64}
import scodec.bits.ByteVector
import java.util.UUID
@ -472,7 +472,7 @@ case class LocalParams(nodeId: PublicKey,
isFunder: Boolean,
defaultFinalScriptPubKey: ByteVector,
walletStaticPaymentBasepoint: Option[PublicKey],
initFeatures: Features)
initFeatures: Features[InitFeature])
/**
* @param initFeatures see [[LocalParams.initFeatures]]
@ -489,7 +489,7 @@ case class RemoteParams(nodeId: PublicKey,
paymentBasepoint: PublicKey,
delayedPaymentBasepoint: PublicKey,
htlcBasepoint: PublicKey,
initFeatures: Features,
initFeatures: Features[InitFeature],
shutdownScript: Option[ByteVector])
case class ChannelFlags(announceChannel: Boolean) {

View File

@ -17,7 +17,7 @@
package fr.acinq.eclair.channel
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
import fr.acinq.eclair.{Feature, FeatureSupport, Features}
import fr.acinq.eclair.{Feature, FeatureScope, FeatureSupport, Features, InitFeature}
/**
* Created by t-bast on 24/06/2021.
@ -28,7 +28,7 @@ import fr.acinq.eclair.{Feature, FeatureSupport, Features}
* 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.
*/
case class ChannelFeatures(features: Set[Feature]) {
case class ChannelFeatures(features: Set[Feature with FeatureScope]) {
val channelType: SupportedChannelType = {
if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) {
@ -45,7 +45,7 @@ case class ChannelFeatures(features: Set[Feature]) {
val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet
val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
def hasFeature(feature: Feature): Boolean = features.contains(feature)
def hasFeature(feature: Feature with FeatureScope): Boolean = features.contains(feature)
override def toString: String = features.mkString(",")
@ -53,13 +53,13 @@ case class ChannelFeatures(features: Set[Feature]) {
object ChannelFeatures {
def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features))
def apply(features: (Feature with FeatureScope)*): ChannelFeatures = ChannelFeatures(Set.from(features))
/** Enrich the channel type with other permanent features that will be applied to the channel. */
def apply(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features): 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,
// such as option_dataloss_protect or option_shutdown_anysegwit.
val availableFeatures: Seq[Feature] = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
val availableFeatures: Seq[Feature with FeatureScope] = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
val allFeatures = channelType.features.toSeq ++ availableFeatures
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. */
sealed trait ChannelType {
/** Features representing that channel type. */
def features: Set[Feature]
def features: Set[Feature with InitFeature]
}
sealed trait SupportedChannelType extends ChannelType {
@ -84,45 +84,45 @@ object ChannelTypes {
// @formatter:off
case object Standard extends SupportedChannelType {
override def features: Set[Feature] = Set.empty
override def features: Set[Feature with InitFeature] = Set.empty
override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
override def toString: String = "standard"
}
case object StaticRemoteKey extends SupportedChannelType {
override def features: Set[Feature] = Set(Features.StaticRemoteKey)
override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey)
override def paysDirectlyToWallet: Boolean = true
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
override def toString: String = "static_remotekey"
}
case object AnchorOutputs extends SupportedChannelType {
override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs)
override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs)
override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat
override def toString: String = "anchor_outputs"
}
case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType {
override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)
override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)
override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
override def toString: String = "anchor_outputs_zero_fee_htlc_tx"
}
case class UnsupportedChannelType(featureBits: Features) extends ChannelType {
override def features: Set[Feature] = featureBits.activated.keySet
case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
override def features: Set[Feature with InitFeature] = featureBits.activated.keySet
override def toString: String = s"0x${featureBits.toByteVector.toHex}"
}
// @formatter:on
private val features2ChannelType: Map[Features, SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx)
.map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType)
private val features2ChannelType: Map[Features[InitFeature], SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx)
.map(channelType => Features[InitFeature](channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType)
.toMap
// NB: Bolt 2: features must exactly match in order to identify a channel type.
def fromFeatures(features: Features): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features))
def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features))
/** Pick the channel type based on local and remote feature bits, as defined by the spec. */
def defaultFromFeatures(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = {
def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): SupportedChannelType = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) {
AnchorOutputsZeroFeeHtlcTx
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
@ -135,7 +135,7 @@ object ChannelTypes {
}
/** Check if a given channel type is compatible with our features. */
def areCompatible(localFeatures: Features, remoteChannelType: ChannelType): Option[SupportedChannelType] = remoteChannelType match {
def areCompatible(localFeatures: Features[InitFeature], remoteChannelType: ChannelType): Option[SupportedChannelType] = remoteChannelType match {
case _: UnsupportedChannelType => None
// We ensure that we support the features necessary for this channel type.
case proposedChannelType: SupportedChannelType =>

View File

@ -79,7 +79,7 @@ object Helpers {
channelConf.minDepthBlocks.max(blocksToReachFunding)
}
def extractShutdownScript(channelId: ByteVector32, localFeatures: Features, remoteFeatures: Features, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = {
def extractShutdownScript(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = {
val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.UpfrontShutdownScript)
val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit)
extractShutdownScript(channelId, canUseUpfrontShutdownScript, canUseAnySegwit, upfrontShutdownScript_opt)
@ -99,7 +99,7 @@ object Helpers {
/**
* Called by the fundee
*/
def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
// BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver:
// MUST reject the channel.
if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash))
@ -154,7 +154,7 @@ object Helpers {
/**
* Called by the funder
*/
def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, remoteFeatures: Features, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
accept.channelType_opt match {
case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt =>
// if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel.
@ -225,7 +225,7 @@ object Helpers {
}
def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = {
val features = Features.empty // empty features for now
val features = Features.empty[FeatureScope] // empty features for now
val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)
val witness = Announcements.generateChannelAnnouncementWitness(
nodeParams.chainHash,

View File

@ -357,7 +357,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
s(e)
}
def createNewChannel(nodeParams: NodeParams, initFeatures: Features, channelType: SupportedChannelType, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = {
def createNewChannel(nodeParams: NodeParams, initFeatures: Features[InitFeature], channelType: SupportedChannelType, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = {
val (finalScript, walletStaticPaymentBasepoint) = if (channelType.paysDirectlyToWallet) {
val walletKey = Helpers.getWalletPaymentBasepoint(wallet)(ExecutionContext.Implicits.global)
(Script.write(Script.pay2wpkh(walletKey)), Some(walletKey))
@ -447,8 +447,8 @@ object Peer {
case class DisconnectedData(channels: Map[FinalChannelId, ActorRef]) extends Data
case class ConnectedData(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data {
val connectionInfo: ConnectionInfo = ConnectionInfo(address, peerConnection, localInit, remoteInit)
def localFeatures: Features = localInit.features
def remoteFeatures: Features = remoteInit.features
def localFeatures: Features[InitFeature] = localInit.features
def remoteFeatures: Features[InitFeature] = remoteInit.features
}
sealed trait State
@ -501,7 +501,7 @@ object Peer {
case class RelayOnionMessage(messageId: ByteVector32, msg: OnionMessage, replyTo_opt: Option[typed.ActorRef[Status]])
// @formatter:on
def makeChannelParams(nodeParams: NodeParams, initFeatures: Features, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = {
def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = {
LocalParams(
nodeParams.nodeId,
nodeParams.channelKeyManager.newFundingKeyPath(isFunder), // we make sure that funder and fundee key path end differently

View File

@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, Logs, TimestampMilli, TimestampSecond}
import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureScope, Features, InitFeature, Logs, TimestampMilli, TimestampSecond}
import scodec.Attempt
import scodec.bits.ByteVector
@ -556,7 +556,7 @@ object PeerConnection {
def outgoing: Boolean = remoteNodeId_opt.isDefined // if this is an outgoing connection, we know the node id in advance
}
case class Authenticated(peerConnection: ActorRef, remoteNodeId: PublicKey) extends RemoteTypes
case class InitializeConnection(peer: ActorRef, chainHash: ByteVector32, features: Features, doSync: Boolean) extends RemoteTypes
case class InitializeConnection(peer: ActorRef, chainHash: ByteVector32, features: Features[InitFeature], doSync: Boolean) extends RemoteTypes
case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes
sealed trait ConnectionResult extends RemoteTypes

View File

@ -18,7 +18,7 @@ package fr.acinq.eclair.payment
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
import scodec.codecs.{list, ubyte}
import scodec.{Codec, Err}
@ -93,7 +93,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat
case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta
}
lazy val features: Features = tags.collectFirst { case f: InvoiceFeatures => f.features }.getOrElse(Features(BitVector.empty))
lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty[InvoiceFeature])
/**
* @return the hash of this payment invoice
@ -141,7 +141,7 @@ object Bolt11Invoice {
Block.LivenetGenesisBlock.hash -> "lnbc"
)
val defaultFeatures: Features = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory))
val defaultFeatures: Features[InvoiceFeature] = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory))
def apply(chainHash: ByteVector32,
amount: Option[MilliSatoshi],
@ -155,7 +155,7 @@ object Bolt11Invoice {
timestamp: TimestampSecond = TimestampSecond.now(),
paymentSecret: ByteVector32 = randomBytes32(),
paymentMetadata: Option[ByteVector] = None,
features: Features = defaultFeatures): Bolt11Invoice = {
features: Features[InvoiceFeature] = defaultFeatures): Bolt11Invoice = {
require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret")
val prefix = prefixes(chainHash)
val tags = {
@ -167,7 +167,8 @@ object Bolt11Invoice {
fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_)),
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
Some(InvoiceFeatures(features))
// We want to keep invoices as small as possible, so we explicitly remove unknown features.
Some(InvoiceFeatures(features.copy(unknown = Set.empty).unscoped()))
).flatten
val routingInfoTags = extraHops.map(RoutingInfo)
defaultTags ++ routingInfoTags
@ -305,7 +306,7 @@ object Bolt11Invoice {
* 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.
*/
def features2bits(features: Features): BitVector = leftPaddedBits(features.toByteVector.bits)
def features2bits[T <: FeatureScope](features: Features[T]): BitVector = leftPaddedBits(features.toByteVector.bits)
private def leftPaddedBits(bits: BitVector): BitVector = {
var highest = -1
@ -371,7 +372,7 @@ object Bolt11Invoice {
/**
* Features supported or required for receiving this payment.
*/
case class InvoiceFeatures(features: Features) extends TaggedField
case class InvoiceFeatures(features: Features[FeatureScope]) extends TaggedField
object Codecs {
@ -414,7 +415,7 @@ object Bolt11Invoice {
.typecase(2, dataCodec(bits).as[UnknownTag2])
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo])
.typecase(4, dataCodec(bits).as[UnknownTag4])
.typecase(5, dataCodec(bits).xmap[Features](Features(_), features2bits).as[InvoiceFeatures])
.typecase(5, dataCodec(bits).xmap[Features[FeatureScope]](Features(_), features2bits).as[InvoiceFeatures])
.typecase(6, dataCodec(bits).as[Expiry])
.typecase(7, dataCodec(bits).as[UnknownTag7])
.typecase(8, dataCodec(bits).as[UnknownTag8])

View File

@ -18,7 +18,7 @@ package fr.acinq.eclair.payment
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, TimestampSecond}
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond}
import scodec.bits.ByteVector
import scala.concurrent.duration.FiniteDuration
@ -44,7 +44,7 @@ trait Invoice {
val minFinalCltvExpiryDelta: Option[CltvExpiryDelta]
val features: Features
val features: Features[InvoiceFeature]
def isExpired(): Boolean = createdAt + relativeExpiry.toSeconds <= TimestampSecond.now()

View File

@ -25,12 +25,13 @@ import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS}
import fr.acinq.eclair.db._
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.{Bolt11Invoice, IncomingPaymentPacket, Invoice, PaymentMetadataReceived, PaymentReceived}
import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentMetadataReceived, PaymentReceived, Invoice}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{FeatureSupport, Features, Logs, MilliSatoshi, NodeParams, randomBytes32}
import fr.acinq.eclair.{FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, randomBytes32}
import scodec.bits.HexStringSyntax
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
import fr.acinq.eclair.payment.Bolt11Invoice
import scala.util.{Failure, Success, Try}
@ -93,9 +94,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
val paymentHash = Crypto.sha256(paymentPreimage)
val desc = Left("Donation")
val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) {
Features(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
} else {
Features(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
Features[InvoiceFeature](Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
}
// Insert a fake invoice and then restart the incoming payment handler
val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = features)
@ -231,7 +232,7 @@ object MultiPartHandler {
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds)
val paymentMetadata = hex"2a"
val invoiceFeatures = if (nodeParams.enableTrampolinePayment) {
Features(nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePayment -> FeatureSupport.Optional))
Features[InvoiceFeature](nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePayment -> FeatureSupport.Optional))
} else {
nodeParams.features.invoiceFeatures()
}

View File

@ -16,7 +16,7 @@
package fr.acinq.eclair.payment.send
import fr.acinq.eclair.Features
import fr.acinq.eclair.{Features, InvoiceFeature}
sealed trait PaymentError extends Throwable
@ -25,7 +25,7 @@ object PaymentError {
// @formatter:off
sealed trait InvalidInvoice extends PaymentError
/** The invoice contains a feature we don't support. */
case class UnsupportedFeatures(features: Features) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" }
case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" }
/** The invoice is missing a payment secret. */
case object PaymentSecretMissing extends InvalidInvoice { override def getMessage: String = "invalid invoice: payment secret is missing" }
// @formatter:on

View File

@ -54,7 +54,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
r.invoice.paymentSecret match {
case _ if !nodeParams.features.areSupported(r.invoice.features) =>
case _ if !nodeParams.features.invoiceFeatures().areSupported(r.invoice.features) =>
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil)
case None =>
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil)

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.QueryChannelRangeTlv.queryFlagsCodec
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, Features}
import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, InitFeature}
import scodec._
import scodec.codecs._
@ -98,7 +98,7 @@ object EclairInternalsSerializer {
("channelQueryChunkSize" | int32) ::
("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf]
val overrideFeaturesListCodec: Codec[List[(PublicKey, Features)]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec))
val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[FeatureScope])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec))
val peerConnectionConfCodec: Codec[PeerConnection.Conf] = (
("authTimeout" | finiteDurationCodec) ::
@ -146,7 +146,7 @@ object EclairInternalsSerializer {
def initializeConnectionCodec(system: ExtendedActorSystem): Codec[PeerConnection.InitializeConnection] = (
("peer" | actorRefCodec(system)) ::
("chainHash" | bytes32) ::
("features" | variableSizeBytes(uint16, featuresCodec)) ::
("features" | variableSizeBytes(uint16, initFeaturesCodec)) ::
("doSync" | bool(8))).as[PeerConnection.InitializeConnection]
def connectionReadyCodec(system: ExtendedActorSystem): Codec[PeerConnection.ConnectionReady] = (

View File

@ -19,7 +19,7 @@ package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult}
import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult}
import scodec.bits.ByteVector
import shapeless.HNil
@ -28,16 +28,16 @@ import shapeless.HNil
*/
object Announcements {
def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features, tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector =
def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[FeatureScope], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector =
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, addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector =
def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[FeatureScope], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector =
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 =
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): ByteVector =
def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[FeatureScope]): ByteVector =
if (isNode1(localNodeId, remoteNodeId)) {
channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty)
} else {
@ -68,7 +68,7 @@ object Announcements {
)
}
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features, timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = {
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = {
require(alias.length <= 32)
val sortedAddresses = nodeAddresses.map {
case address@(_: IPv4) => (1, address)
@ -76,8 +76,7 @@ object Announcements {
case address@(_: Tor2) => (3, address)
case address@(_: Tor3) => (4, address)
}.sortBy(_._1).map(_._2)
val nodeAnnouncementFeatures = features.nodeAnnouncementFeatures()
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, nodeAnnouncementFeatures, sortedAddresses, TlvStream.empty)
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream.empty)
val sig = Crypto.sign(witness, nodeSecret)
NodeAnnouncement(
signature = sig,
@ -85,7 +84,7 @@ object Announcements {
nodeId = nodeSecret.publicKey,
rgbColor = color,
alias = alias,
features = nodeAnnouncementFeatures,
features = features.unscoped(),
addresses = sortedAddresses
)
}

View File

@ -95,7 +95,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm
// on restart we update our node announcement
// note that if we don't currently have public channels, this will be ignored
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features)
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures())
self ! nodeAnn
log.info(s"initialization completed, ready to process messages")

View File

@ -114,7 +114,7 @@ object Validation {
// in case we just validated our first local channel, we announce the local node
if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) {
log.info("first local channel validated, announcing local node")
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features)
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures())
ctx.self ! nodeAnn
}
// public channels that haven't yet been announced are considered as private channels

View File

@ -26,7 +26,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, TimestampSecond}
import fr.acinq.eclair.{BlockHeight, Features, InitFeature, TimestampSecond}
import scodec.Codec
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._

View File

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

View File

@ -18,7 +18,6 @@ package fr.acinq.eclair.wire.internal.channel.version1
import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath}
import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut}
import fr.acinq.eclair.BlockHeight
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions._
@ -28,6 +27,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, Features, InitFeature}
import scodec.bits.ByteVector
import scodec.codecs._
import scodec.{Attempt, Codec}

View File

@ -18,7 +18,6 @@ package fr.acinq.eclair.wire.internal.channel.version2
import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath}
import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut}
import fr.acinq.eclair.BlockHeight
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions._
@ -28,6 +27,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, Features, InitFeature}
import scodec.bits.ByteVector
import scodec.codecs._
import scodec.{Attempt, Codec}

View File

@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc,
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.UpdateMessage
import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features}
import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, InitFeature}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec}

View File

@ -42,7 +42,7 @@ object ChannelTlv {
case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv
val channelTypeCodec: Codec[ChannelTypeTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap(
b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b))),
b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b).initFeatures())),
tlv => Features(tlv.channelType.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector
)

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.protocol.CommonCodecs._
import fr.acinq.eclair.{Features, KamonExt}
import fr.acinq.eclair.{FeatureScope, Features, InitFeature, KamonExt, NodeFeature}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec}
@ -29,18 +29,20 @@ import shapeless._
*/
object LightningMessageCodecs {
val featuresCodec: Codec[Features] = varsizebinarydata.xmap[Features](
val featuresCodec: Codec[Features[FeatureScope]] = varsizebinarydata.xmap[Features[FeatureScope]](
{ bytes => Features(bytes) },
{ features => features.toByteVector }
)
val initFeaturesCodec: Codec[Features[InitFeature]] = featuresCodec.xmap[Features[InitFeature]](_.initFeatures(), _.unscoped())
/** For historical reasons, features are divided into two feature bitmasks. We only send from the second one, but we allow receiving in both. */
val combinedFeaturesCodec: Codec[Features] = (
val combinedFeaturesCodec: Codec[Features[InitFeature]] = (
("globalFeatures" | varsizebinarydata) ::
("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features](
("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features[InitFeature]](
{ case (gf, lf) =>
val length = gf.length.max(lf.length)
Features(gf.padLeft(length) | lf.padLeft(length))
Features(gf.padLeft(length) | lf.padLeft(length)).initFeatures()
},
{ features => (ByteVector.empty, 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.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.{ChannelFlags, ChannelType}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, FeatureScope, Features, InitFeature, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, UInt64}
import scodec.bits.ByteVector
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
@ -47,7 +47,7 @@ sealed trait UpdateMessage extends HtlcMessage // <- not in the spec
sealed trait HtlcSettlementMessage extends UpdateMessage { def id: Long } // <- not in the spec
// @formatter:on
case class Init(features: Features, tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage {
case class Init(features: Features[InitFeature], tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage {
val networks = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil)
val remoteAddress_opt = tlvStream.get[InitTlv.RemoteAddress].map(_.address)
}
@ -200,7 +200,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64,
nodeSignature2: ByteVector64,
bitcoinSignature1: ByteVector64,
bitcoinSignature2: ByteVector64,
features: Features,
features: Features[FeatureScope],
chainHash: ByteVector32,
shortChannelId: ShortChannelId,
nodeId1: PublicKey,
@ -263,7 +263,7 @@ case class Tor3(tor3: String, port: Int) extends OnionAddress { override def soc
// @formatter:on
case class NodeAnnouncement(signature: ByteVector64,
features: Features,
features: Features[FeatureScope],
timestamp: TimestampSecond,
nodeId: PublicKey,
rgbColor: Color,

View File

@ -109,7 +109,7 @@ class FeaturesSpec extends AnyFunSuite {
}
test("features compatibility") {
case class TestCase(ours: Features, theirs: Features, oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean)
case class TestCase(ours: Features[FeatureScope], theirs: Features[FeatureScope], oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean)
val testCases = Seq(
// Empty features
TestCase(
@ -168,7 +168,7 @@ class FeaturesSpec extends AnyFunSuite {
// They have unknown optional features
TestCase(
Features(VariableLengthOnion -> Optional),
Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(141))),
Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(141))),
oursSupportTheirs = true,
theirsSupportOurs = true,
compatible = true
@ -176,7 +176,7 @@ class FeaturesSpec extends AnyFunSuite {
// They have unknown mandatory features
TestCase(
Features(VariableLengthOnion -> Optional),
Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(142))),
Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(142))),
oursSupportTheirs = false,
theirsSupportOurs = true,
compatible = false
@ -198,10 +198,10 @@ class FeaturesSpec extends AnyFunSuite {
compatible = false
),
// nonreg testing of future features (needs to be updated with every new supported mandatory bit)
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], 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[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], 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),
)
for (testCase <- testCases) {
@ -212,19 +212,22 @@ class FeaturesSpec extends AnyFunSuite {
}
test("filter features based on their usage") {
val features = Features(
Map[Feature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
Set(UnknownFeature(753), UnknownFeature(852))
val features = Features[FeatureScope](
Map[Feature with FeatureScope, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
)
assert(features.initFeatures() === Features(
Map[Feature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852))
assert(features.initFeatures() === Features[InitFeature](
Map[Feature with InitFeature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
))
assert(features.nodeAnnouncementFeatures() === Features(
Map[Feature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852))
assert(features.nodeAnnouncementFeatures() === Features[NodeFeature](
Map[Feature with NodeFeature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
))
assert(features.invoiceFeatures() === Features[InvoiceFeature](
Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303))
))
assert(features.invoiceFeatures() === Features(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional))
}
test("features to bytes") {
@ -232,8 +235,8 @@ class FeaturesSpec extends AnyFunSuite {
hex"" -> Features.empty,
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[Feature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))),
hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30)))
hex"09004200" -> Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](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)))
)
for ((bin, features) <- testCases) {
@ -249,16 +252,14 @@ class FeaturesSpec extends AnyFunSuite {
{
val conf = ConfigFactory.parseString(
"""
|features {
| option_data_loss_protect = optional
| initial_routing_sync = optional
| gossip_queries = optional
| gossip_queries_ex = optional
| var_onion_optin = optional
| payment_secret = optional
| basic_mpp = optional
|}
""".stripMargin)
option_data_loss_protect = optional
initial_routing_sync = optional
gossip_queries = optional
gossip_queries_ex = optional
var_onion_optin = optional
payment_secret = optional
basic_mpp = optional
""")
val features = fromConfiguration(conf)
assert(features.toByteVector === hex"028a8a")
@ -276,16 +277,12 @@ class FeaturesSpec extends AnyFunSuite {
{
val conf = ConfigFactory.parseString(
"""
| features {
| initial_routing_sync = optional
| option_data_loss_protect = optional
| gossip_queries = optional
| gossip_queries_ex = mandatory
| var_onion_optin = optional
| }
|
""".stripMargin
)
initial_routing_sync = optional
option_data_loss_protect = optional
gossip_queries = optional
gossip_queries_ex = mandatory
var_onion_optin = optional
""")
val features = fromConfiguration(conf)
assert(features.toByteVector === hex"068a")
@ -303,29 +300,21 @@ class FeaturesSpec extends AnyFunSuite {
{
val confWithUnknownFeatures = ConfigFactory.parseString(
"""
|features {
| option_non_existent = mandatory # this is ignored
| gossip_queries = optional
| payment_secret = mandatory
|}
""".stripMargin)
option_non_existent = mandatory
gossip_queries = optional
payment_secret = mandatory
""")
val features = fromConfiguration(confWithUnknownFeatures)
assert(features.toByteVector === hex"4080")
assert(Features(hex"4080") === features)
assert(features.hasFeature(ChannelRangeQueries, Some(Optional)))
assert(features.hasFeature(PaymentSecret, Some(Mandatory)))
assertThrows[RuntimeException](fromConfiguration(confWithUnknownFeatures))
}
{
val confWithUnknownSupport = ConfigFactory.parseString(
"""
|features {
| option_data_loss_protect = what
| gossip_queries = optional
| payment_secret = mandatory
|}
""".stripMargin)
option_data_loss_protect = what
gossip_queries = optional
payment_secret = mandatory
""")
assertThrows[RuntimeException](fromConfiguration(confWithUnknownSupport))
}
@ -333,14 +322,12 @@ class FeaturesSpec extends AnyFunSuite {
{
val confWithDisabledFeatures = ConfigFactory.parseString(
"""
|features {
| option_data_loss_protect = disabled
| gossip_queries = optional
| payment_secret = mandatory
| option_support_large_channel = disabled
| gossip_queries_ex = mandatory
|}
""".stripMargin)
option_data_loss_protect = disabled
gossip_queries = optional
payment_secret = mandatory
option_support_large_channel = disabled
gossip_queries_ex = mandatory
""")
val features = fromConfiguration(confWithDisabledFeatures)
assert(!features.hasFeature(DataLossProtect))

View File

@ -166,7 +166,7 @@ class StartupSpec extends AnyFunSuite {
test("parse human readable override features") {
val perNodeConf = ConfigFactory.parseString(
"""
| override-features = [ // optional per-node features
| override-init-features = [ // optional per-node features
| {
| nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
| features {
@ -185,10 +185,10 @@ class StartupSpec extends AnyFunSuite {
assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional))
}
test("filter out non-init features in node override") {
test("reject non-init features in node override") {
val perNodeConf = ConfigFactory.parseString(
"""
| override-features = [ // optional per-node features
| override-init-features = [ // optional per-node features
| {
| nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
| features {
@ -211,15 +211,7 @@ class StartupSpec extends AnyFunSuite {
""".stripMargin
)
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
val perNodeFeaturesA = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
val perNodeFeaturesB = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
val defaultNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")))
// Some features should never be sent in init messages.
assert(nodeParams.features.hasFeature(PaymentMetadata))
assert(!perNodeFeaturesA.hasFeature(PaymentMetadata))
assert(!perNodeFeaturesB.hasFeature(PaymentMetadata))
assert(!defaultNodeFeatures.hasFeature(PaymentMetadata))
assertThrows[IllegalArgumentException](makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)))
}
test("override feerate mismatch tolerance") {

View File

@ -84,8 +84,8 @@ object TestConstants {
color = Color(1, 2, 3),
publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil,
torAddress_opt = None,
features = Features(
Map[Feature, FeatureSupport](
features = Features[FeatureScope](
Map[Feature with FeatureScope, FeatureSupport](
DataLossProtect -> Optional,
ChannelRangeQueries -> Optional,
ChannelRangeQueriesExtended -> Optional,
@ -97,7 +97,7 @@ object TestConstants {
Set(UnknownFeature(TestFeature.optional))
),
pluginParams = List(pluginParams),
overrideFeatures = Map.empty,
overrideInitFeatures = Map.empty,
syncWhitelist = Set.empty,
channelConf = ChannelConf(
dustLimit = 1100 sat,
@ -199,7 +199,7 @@ object TestConstants {
def channelParams: LocalParams = Peer.makeChannelParams(
nodeParams,
nodeParams.features,
nodeParams.features.initFeatures(),
Script.write(Script.pay2wpkh(randomKey().publicKey)),
None,
isFunder = true,
@ -232,7 +232,7 @@ object TestConstants {
PaymentMetadata -> Optional,
),
pluginParams = Nil,
overrideFeatures = Map.empty,
overrideInitFeatures = Map.empty,
syncWhitelist = Set.empty,
channelConf = ChannelConf(
dustLimit = 1000 sat,
@ -334,7 +334,7 @@ object TestConstants {
def channelParams: LocalParams = Peer.makeChannelParams(
nodeParams,
nodeParams.features,
nodeParams.features.initFeatures(),
Script.write(Script.pay2wpkh(randomKey().publicKey)),
None,
isFunder = false,

View File

@ -20,7 +20,7 @@ import fr.acinq.eclair.FeatureSupport._
import fr.acinq.eclair.Features._
import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.{Features, TestKitBaseClass}
import fr.acinq.eclair.{Features, InitFeature, TestKitBaseClass}
import org.scalatest.funsuite.AnyFunSuiteLike
class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods {
@ -55,7 +55,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha
}
test("pick channel type based on local and remote features") {
case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelType: ChannelType)
case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], expectedChannelType: ChannelType)
val testCases = Seq(
TestCase(Features.empty, Features.empty, ChannelTypes.Standard),
TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard),
@ -81,28 +81,28 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha
test("create channel type from features") {
val validChannelTypes = Seq(
Features.empty -> ChannelTypes.Standard,
Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey,
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs,
Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx,
Features.empty[InitFeature] -> ChannelTypes.Standard,
Features[InitFeature](StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey,
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs,
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx,
)
for ((features, expected) <- validChannelTypes) {
assert(ChannelTypes.fromFeatures(features) === expected)
}
val invalidChannelTypes = Seq(
Features(Wumbo -> Optional),
Features(StaticRemoteKey -> Optional),
Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional),
Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional),
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional),
Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory),
Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional),
Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional),
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional),
Features[InitFeature](Wumbo -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional),
Features[InitFeature](StaticRemoteKey -> Mandatory, Wumbo -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Optional),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional),
Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory),
Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional),
)
for (features <- invalidChannelTypes) {
assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features))
@ -110,15 +110,15 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha
}
test("enrich channel type with other permanent channel features") {
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty).features.isEmpty)
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).features === Set(Wumbo))
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional)).features === Set(Wumbo))
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty).features === Set(StaticRemoteKey))
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, Wumbo))
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputs))
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputs, Wumbo))
assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty, Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx))
assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo))
assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features.isEmpty)
assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features === Set(Wumbo))
assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Mandatory), Features[InitFeature](Wumbo -> Optional)).features === Set(Wumbo))
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features === Set(StaticRemoteKey))
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, Wumbo))
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputs))
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputs, Wumbo))
assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx))
assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo))
}
}

View File

@ -38,9 +38,9 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan
}
}
def aliceInit = Init(Alice.nodeParams.features)
def aliceInit = Init(Alice.nodeParams.features.initFeatures())
def bobInit = Init(Bob.nodeParams.features)
def bobInit = Init(Bob.nodeParams.features.initFeatures())
test("use funding pubkeys from publish commitment to spend our output") { f =>
import f._

View File

@ -146,6 +146,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional))
.initFeatures()
val bobInitFeatures = Bob.nodeParams.features
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional))
@ -154,6 +155,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional))
.initFeatures()
val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures)

View File

@ -57,8 +57,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
}
val aliceInit = Init(TestConstants.Alice.nodeParams.features)
val bobInit = Init(TestConstants.Bob.nodeParams.features)
val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures())
val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures())
test("re-send lost htlc and signature after first commitment") { f =>
import f._

View File

@ -1018,8 +1018,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// then we manually replace alice's state with an older one
alice.setState(OFFLINE, oldStateData)
// then we reconnect them
val aliceInit = Init(TestConstants.Alice.nodeParams.features)
val bobInit = Init(TestConstants.Bob.nodeParams.features)
val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures())
val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures())
alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)
bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)
// peers exchange channel_reestablish messages

View File

@ -67,13 +67,13 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
withFixture(test.toNoArgTest(FixtureParam(aliceParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer)))
}
def connect(aliceParams: NodeParams, remoteNodeId: PublicKey, switchboard: TestProbe, router: TestProbe, connection: TestProbe, transport: TestProbe, peerConnection: TestFSMRef[PeerConnection.State, PeerConnection.Data, PeerConnection], peer: TestProbe, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features), doSync: Boolean = false, isPersistent: Boolean = true): Unit = {
def connect(aliceParams: NodeParams, remoteNodeId: PublicKey, switchboard: TestProbe, router: TestProbe, connection: TestProbe, transport: TestProbe, peerConnection: TestFSMRef[PeerConnection.State, PeerConnection.Data, PeerConnection], peer: TestProbe, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures()), doSync: Boolean = false, isPersistent: Boolean = true): Unit = {
// let's simulate a connection
val probe = TestProbe()
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = isPersistent))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features, doSync))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features.initFeatures(), doSync))
transport.expectMsgType[TransportHandler.Listener]
val localInit = transport.expectMsgType[protocol.Init]
assert(localInit.networks === List(Block.RegtestGenesisBlock.hash))
@ -101,7 +101,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.send(peerConnection, incomingConnection)
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = false))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = false))
transport.expectMsgType[TransportHandler.Listener]
val localInit = transport.expectMsgType[protocol.Init]
assert(localInit.remoteAddress_opt === Some(fakeIPAddress))
@ -133,7 +133,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(peerConnection)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
probe.expectTerminated(peerConnection, nodeParams.peerConnectionConf.initTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here
origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("initialization timed out"))
}
@ -145,7 +145,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value)
@ -161,7 +161,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value)
@ -177,7 +177,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
// remote activated MPP but forgot payment secret
@ -194,10 +194,10 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
probe.watch(transport.ref)
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true))
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true))
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
transport.expectMsgType[TransportHandler.Listener]
transport.expectMsgType[protocol.Init]
transport.send(peerConnection, protocol.Init(Bob.nodeParams.features, TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil))))
transport.send(peerConnection, protocol.Init(Bob.nodeParams.features.initFeatures(), TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil))))
transport.expectMsgType[TransportHandler.ReadAck]
probe.expectTerminated(transport.ref)
origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible networks"))

View File

@ -86,10 +86,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
withFixture(test.toNoArgTest(FixtureParam(aliceParams, remoteNodeId, peer, peerConnection, channel, switchboard)))
}
def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[HasCommitments] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features)): Unit = {
def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[HasCommitments] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures())): Unit = {
// let's simulate a connection
switchboard.send(peer, Peer.Init(channels))
val localInit = protocol.Init(peer.underlyingActor.nodeParams.features)
val localInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures())
switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, localInit, remoteInit))
val probe = TestProbe()
probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped)))
@ -257,7 +257,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
monitor.expectMsg(FSM.Transition(reconnectionTask, ReconnectionTask.WAITING, ReconnectionTask.CONNECTING))
// we simulate a success
val dummyInit = protocol.Init(peer.underlyingActor.nodeParams.features)
val dummyInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures())
probe.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, dummyInit, dummyInit))
// we make sure that the reconnection task has done a full circle
@ -353,7 +353,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
}
// They want to use a channel type we don't support yet.
{
val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map[Feature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22)))))))
val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features[InitFeature](Map[Feature with InitFeature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22)))))))
peerConnection.send(peer, open)
peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard"))
}

View File

@ -10,7 +10,7 @@ import fr.acinq.eclair.channel.ChannelIdAssigned
import fr.acinq.eclair.io.Switchboard._
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Features, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey}
import fr.acinq.eclair.{Features, InitFeature, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey}
import org.scalatest.funsuite.AnyFunSuiteLike
import scodec.bits._
@ -63,7 +63,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
peer.expectMsg(Peer.Disconnect(remoteNodeId))
}
def sendFeatures(nodeParams: NodeParams, remoteNodeId: PublicKey, expectedFeatures: Features, expectedSync: Boolean): Unit = {
def sendFeatures(nodeParams: NodeParams, remoteNodeId: PublicKey, expectedFeatures: Features[InitFeature], expectedSync: Boolean): Unit = {
val (probe, peer, peerConnection) = (TestProbe(), TestProbe(), TestProbe())
val switchboard = TestActorRef(new Switchboard(nodeParams, FakePeerFactory(probe, peer)))
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)

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.Features.{PaymentMetadata, PaymentSecret, _}
import fr.acinq.eclair.payment.Bolt11Invoice._
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion}
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32, randomKey}
import org.scalatest.funsuite.AnyFunSuite
import scodec.DecodeResult
import scodec.bits._
@ -41,6 +41,46 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
val nodeId = pub
assert(nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
// Copy of Bolt11Invoice.apply that doesn't strip unknown features
def createInvoiceUnsafe(chainHash: ByteVector32,
amount: Option[MilliSatoshi],
paymentHash: ByteVector32,
privateKey: PrivateKey,
description: Either[String, ByteVector32],
minFinalCltvExpiryDelta: CltvExpiryDelta,
fallbackAddress: Option[String] = None,
expirySeconds: Option[Long] = None,
extraHops: List[List[ExtraHop]] = Nil,
timestamp: TimestampSecond = TimestampSecond.now(),
paymentSecret: ByteVector32 = randomBytes32(),
paymentMetadata: Option[ByteVector] = None,
features: Features[FeatureScope] = defaultFeatures.unscoped()): Bolt11Invoice = {
require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret")
val prefix = prefixes(chainHash)
val tags = {
val defaultTags = List(
Some(PaymentHash(paymentHash)),
Some(description.fold(Description, DescriptionHash)),
Some(Bolt11Invoice.PaymentSecret(paymentSecret)),
paymentMetadata.map(Bolt11Invoice.PaymentMetadata),
fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_)),
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
Some(InvoiceFeatures(features))
).flatten
val routingInfoTags = extraHops.map(RoutingInfo)
defaultTags ++ routingInfoTags
}
Bolt11Invoice(
prefix = prefix,
amount_opt = amount,
createdAt = timestamp,
nodeId = privateKey.publicKey,
tags = tags,
signature = ByteVector.empty
).sign(privateKey)
}
test("check minimal unit is used") {
assert('p' === Amount.unit(1 msat))
assert('p' === Amount.unit(99 msat))
@ -97,7 +137,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
assert(invoice.amount_opt.isEmpty)
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(invoice.paymentSecret.map(_.bytes) === Some(hex"1111111111111111111111111111111111111111111111111111111111111111"))
assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory))
assert(invoice.features === Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory))
assert(invoice.createdAt == TimestampSecond(1496314658L))
assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(invoice.description == Left("Please consider supporting this project"))
@ -112,7 +152,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(250000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory))
assert(invoice.features === Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory))
assert(invoice.createdAt == TimestampSecond(1496314658L))
assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(invoice.description == Left("1 cup coffee"))
@ -445,8 +485,8 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
)
for ((features, res) <- featureBits) {
val invoice = Bolt11Invoice(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.areSupported(invoice.features)) === res)
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) === invoice)
}
}
@ -510,69 +550,85 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("nonreg") {
val requests = List(
"lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl",
"lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd",
"lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex",
"lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque",
"lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0",
"lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54",
"lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe",
"lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf",
"lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc",
"lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3",
"lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw",
"lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28",
"lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah",
"lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv",
"lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d",
"lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9",
"lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3",
"lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t",
"lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee",
"lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6",
"lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h",
"lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv",
"lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww",
"lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr",
"lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad",
"lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw",
"lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e",
"lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr",
"lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u",
"lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j",
"lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5",
"lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda",
"lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu",
"lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu",
"lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733",
"lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh",
"lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7",
"lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g",
"lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v",
"lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0",
"lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838",
"lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx",
"lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer",
"lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr",
"lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0",
"lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x",
"lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra",
"lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r",
"lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w",
"lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y",
"lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53",
"lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz",
"lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua",
"lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p",
"lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx",
"lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9",
"lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9",
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu",
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk"
"lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"),
"lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah" -> PublicKey(hex"039f01ad62e5208940faff11d0bbc997582eafad7642aaf53de6a5f6551ab73400"),
"lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"),
"lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee" -> PublicKey(hex"03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda"),
"lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"),
"lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"),
"lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"),
"lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"),
"lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"),
"lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"),
"lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"),
"lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"),
"lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"),
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"),
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"),
"lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qrjgq7md5lu2hhkz657rs2a40xm2elaqda4krv6vy44my49x02azsqwr35puvgzjltd6dfth2awxcq49cx3srkl3zl34xhw7ppv840yf74wqq88rwr5" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
"lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qr3gqjdynggx20rz4nh98uknmtp2wkwk95zru8lfmw0cz9s3t0xpevuzpzz4k34cprpg9jfc3yp8zc827psug69j4w4pkn70rrfddcqf9wnqqcm2nc4" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
"lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9q9sqsgqruuf6y6hd77533p6ufl3dapzzt55uj7t88mgty7hvfpy5lzvntpyn82j72fr3wqz985lh7l2f5pnju66nman5z09p24qvp2k8443skqqq38n4w" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
"lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qxpqqsgqh88td9f8p8ls8r6devh9lhvppwqe6e0lkvehyu8ztu76m9s8nu2x0rfp5z9jmn2ta97mex2ne6yecvtz8r0qej62lvkngpaduhgytncqts4cxs" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"),
)
for (req <- requests) {
assert(Bolt11Invoice.fromString(req).toString == req)
for ((req, nodeId) <- requests) {
assert(Bolt11Invoice.fromString(req).nodeId === nodeId)
assert(Bolt11Invoice.fromString(req).toString === req)
}
}
test("no unknown feature in invoice"){
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())
assert(invoice.features === Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory)))
assert(Bolt11Invoice.fromString(invoice.toString) === invoice)
}
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)))))
}
}

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

View File

@ -37,7 +37,7 @@ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv}
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax}
@ -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)
val featuresWithoutMpp: Features = Features(
val featuresWithoutMpp: Features[InvoiceFeature] = Features[InvoiceFeature](
VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory
)
val featuresWithMpp: Features = Features(
val featuresWithMpp: Features[InvoiceFeature] = Features[InvoiceFeature](
VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional,
)
val featuresWithTrampoline: Features = Features(
val featuresWithTrampoline: Features[InvoiceFeature] = Features[InvoiceFeature](
VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional,
@ -86,7 +86,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
override def withFixture(test: OneArgTest): Outcome = {
val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp
val nodeParams = TestConstants.Alice.nodeParams.copy(features = features)
val nodeParams = TestConstants.Alice.nodeParams.copy(features = features.unscoped())
val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe())
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
@ -128,7 +128,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
Bolt11Invoice.Description("Some invoice"),
Bolt11Invoice.PaymentSecret(randomBytes32()),
Bolt11Invoice.Expiry(3600),
Bolt11Invoice.InvoiceFeatures(Features(bin"000001000000000000000000000000000100000100000000")) // feature 42
Bolt11Invoice.InvoiceFeatures(Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(42))).unscoped())
)
val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty)
val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)

View File

@ -31,7 +31,7 @@ import fr.acinq.eclair.transactions.Transactions.InputInfo
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey}
import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuite
import scodec.Attempt
@ -212,7 +212,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// a -> b -> c d -> e
val routingHints = List(List(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144))))
val invoiceFeatures = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203"))
val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None))
assert(amount_ac === amount_bc)

View File

@ -38,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode
import fr.acinq.eclair.router.Router.RouteRequest
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scodec.bits.HexStringSyntax
@ -567,7 +567,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive an upstream multi-part payment.
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
val features = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features)
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice

View File

@ -22,7 +22,8 @@ import fr.acinq.eclair.TestConstants.Alice
import fr.acinq.eclair._
import fr.acinq.eclair.router.Announcements._
import fr.acinq.eclair.wire.protocol.ChannelUpdate.ChannelFlags
import fr.acinq.eclair.wire.protocol.NodeAddress
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.nodeAnnouncementCodec
import fr.acinq.eclair.wire.protocol.{Color, NodeAddress}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits._
@ -53,7 +54,7 @@ class AnnouncementsSpec extends AnyFunSuite {
}
test("create valid signed node announcement") {
val features = Features(
val features = Features[FeatureScope](
Features.DataLossProtect -> FeatureSupport.Optional,
Features.InitialRoutingSync -> FeatureSupport.Optional,
Features.ChannelRangeQueries -> FeatureSupport.Optional,
@ -63,7 +64,7 @@ class AnnouncementsSpec extends AnyFunSuite {
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
Features.PaymentMetadata -> FeatureSupport.Optional,
)
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features)
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features.nodeAnnouncementFeatures())
// Features should be filtered to only include node_announcement related features.
assert(ann.features === Features(
Features.DataLossProtect -> FeatureSupport.Optional,
@ -84,7 +85,7 @@ class AnnouncementsSpec extends AnyFunSuite {
NodeAddress.fromParts("140.82.121.4", 9735).get,
NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get,
)
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features)
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features.nodeAnnouncementFeatures())
assert(checkSig(ann))
assert(ann.addresses === List(
NodeAddress.fromParts("140.82.121.4", 9735).get,
@ -128,4 +129,12 @@ class AnnouncementsSpec extends AnyFunSuite {
assert(!channelUpdate2_disabled.channelFlags.isEnabled)
}
test("announce irrelevant features") {
// This announcement has option_payment_metadata which is not a node feature
val encoded = hex"25d8bf19e2c6562a85b3109122118a50728d42c9054537b5a26c1e982407f0923a6f162cf0939bfaf58d7e7a7a7c86d7dfd02dda9b0d2036e8fd35a7afc78daa00070100000000000061fcfc1d039093816bb908c043341f3ee47ea86179627d26aa00704593503e684a0b9a38cb01020374657374206e6f646500000000000000000000000000000000000000000000000007018c5279042607"
val ann = nodeAnnouncementCodec.decode(encoded.bits).require.value
assert(ann.features.hasFeature(Features.PaymentMetadata))
assert(checkSig(ann))
assert(nodeAnnouncementCodec.encode(ann).require.bytes === encoded)
}
}

View File

@ -66,7 +66,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi
// in the tests we are 'a', we don't define a node_a, it will be generated automatically when the router validates the first channel
val node_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features.empty)
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features)
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures())
val node_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features.empty)
val node_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty)
val node_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty)

View File

@ -55,7 +55,7 @@ class RouterSpec extends BaseRouterSpec {
// valid channel announcement, no stashing
val chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c)
val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, chan_ac.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum)
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features, timestamp = TimestampSecond.now() + 1)
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac))
peerConnection.expectNoMessage(100 millis) // we don't immediately acknowledge the announcement (back pressure)
assert(watcher.expectMsgType[ValidateRequest].ann === chan_ac)
@ -173,7 +173,7 @@ class RouterSpec extends BaseRouterSpec {
// unknown channel
val priv_y = randomKey()
val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, ShortChannelId(4646464), CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum)
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features)
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures())
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay))
peerConnection.expectMsg(TransportHandler.ReadAck(update_ay))
peerConnection.expectMsg(GossipDecision.NoRelatedChannel(update_ay))
@ -191,7 +191,7 @@ class RouterSpec extends BaseRouterSpec {
val priv_funding_y = randomKey() // a-y will have an invalid script
val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y)
val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum)
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features)
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures())
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay))
assert(watcher.expectMsgType[ValidateRequest].ann === chan_ay)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay))

View File

@ -352,7 +352,7 @@ object RoutingSyncSpec {
val channelAnn_12 = channelAnnouncement(shortChannelId, priv1, priv2, priv_funding1, priv_funding2)
val channelUpdate_12 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv1, priv2.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp)
val channelUpdate_21 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv2, priv1.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp)
val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features)
val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures())
val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "b", Color(0, 0, 0), List(), Features.empty)
val publicChannel = PublicChannel(channelAnn_12, ByteVector32.Zeroes, Satoshi(0), Some(channelUpdate_12), Some(channelUpdate_21), None)
(publicChannel, nodeAnnouncement_1, nodeAnnouncement_2)

View File

@ -70,7 +70,7 @@ class ChannelCodecs1Spec extends AnyFunSuite {
defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)),
walletStaticPaymentBasepoint = None,
isFunder = Random.nextBoolean(),
initFeatures = Features(randomBytes(256)))
initFeatures = Features(randomBytes(256)).initFeatures())
val o1 = o.copy(walletStaticPaymentBasepoint = Some(PrivateKey(randomBytes32()).publicKey))
roundtrip(o, localParamsCodec(ChannelVersion.ZEROES))
@ -92,7 +92,7 @@ class ChannelCodecs1Spec extends AnyFunSuite {
paymentBasepoint = randomKey().publicKey,
delayedPaymentBasepoint = randomKey().publicKey,
htlcBasepoint = randomKey().publicKey,
initFeatures = TestConstants.Alice.nodeParams.features,
initFeatures = TestConstants.Alice.nodeParams.features.initFeatures(),
shutdownScript = None)
val encoded = remoteParamsCodec.encode(o).require
val decoded = remoteParamsCodec.decodeValue(encoded).require

View File

@ -18,6 +18,8 @@ package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong}
import fr.acinq.eclair.FeatureSupport.Optional
import fr.acinq.eclair.Features.DataLossProtect
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
@ -269,7 +271,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil)
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 node_announcement = NodeAnnouncement(randomBytes64(), Features(bin(1, 2)), 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[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 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 gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500)

View File

@ -320,12 +320,12 @@ object FrontRouterSpec {
val (priv_funding_a, priv_funding_b, priv_funding_c, priv_funding_d, priv_funding_e, priv_funding_f) = (randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey())
val (funding_a, funding_b, funding_c, funding_d, funding_e, funding_f) = (priv_funding_a.publicKey, priv_funding_b.publicKey, priv_funding_c.publicKey, priv_funding_d.publicKey, priv_funding_e.publicKey, priv_funding_f.publicKey)
val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, Features(hex"0200"))
val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features(hex""))
val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, Features(hex"0200"))
val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features(hex"00"))
val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features(hex"00"))
val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features(hex"00"))
val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, Features(Features.VariableLengthOnion -> FeatureSupport.Optional))
val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features.empty)
val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, Features(Features.VariableLengthOnion -> FeatureSupport.Optional))
val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features.empty)
val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty)
val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty)
val channelId_ab = ShortChannelId(BlockHeight(420000), 1, 0)
val channelId_bc = ShortChannelId(BlockHeight(420000), 2, 0)