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:
parent
553727cb22
commit
9401592382
@ -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 {
|
||||
|
@ -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 { }
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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 =>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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] = (
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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._
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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) })
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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") {
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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._
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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._
|
||||
|
@ -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
|
||||
|
@ -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"))
|
||||
|
@ -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"))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)))))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user