mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-13 19:37:35 +01:00
Merge branch 'master' into android-merge
This commit is contained in:
commit
d77e9664c9
71 changed files with 1040 additions and 655 deletions
|
@ -4,7 +4,7 @@ services:
|
|||
dist: trusty
|
||||
language: scala
|
||||
scala:
|
||||
- 2.13.1
|
||||
- 2.11.12
|
||||
env:
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib
|
||||
before_install:
|
||||
|
@ -23,4 +23,4 @@ jdk:
|
|||
- openjdk11
|
||||
notifications:
|
||||
email:
|
||||
- ops@acinq.fr
|
||||
- ops@acinq.fr
|
||||
|
|
|
@ -37,11 +37,18 @@ eclair {
|
|||
node-color = "49daaa"
|
||||
|
||||
trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag
|
||||
features = "0a8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex + variable_length_onion
|
||||
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md
|
||||
features {
|
||||
initial_routing_sync = optional
|
||||
option_data_loss_protect = optional
|
||||
gossip_queries = optional
|
||||
gossip_queries_ex = optional
|
||||
var_onion_optin = optional
|
||||
}
|
||||
override-features = [ // optional per-node features
|
||||
# {
|
||||
# nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
# features = ""
|
||||
# features { }
|
||||
# }
|
||||
]
|
||||
sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers
|
||||
|
@ -178,4 +185,22 @@ eclair {
|
|||
executor = "thread-pool-executor"
|
||||
type = PinnedDispatcher
|
||||
}
|
||||
}
|
||||
|
||||
akka {
|
||||
io {
|
||||
tcp {
|
||||
# The maximum number of bytes delivered by a `Received` message. Before
|
||||
# more data is read from the network the connection actor will try to
|
||||
# do other work.
|
||||
# The purpose of this setting is to impose a smaller limit than the
|
||||
# configured receive buffer size. When using value 'unlimited' it will
|
||||
# try to read all from the receive buffer.
|
||||
# As per BOLT#8 lightning messages are at most 2 + 16 + 65535 + 16 = 65569bytes
|
||||
# Currently the largest message is update_add_htlc (~1500b).
|
||||
# As a tradeoff to reduce the RAM consumption, in conjunction with tcp pull mode,
|
||||
# the default value is chosen to allow for a decent number of messages to be prefetched.
|
||||
max-received-message-size = 16384b
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,8 +52,7 @@ class CheckElectrumSetup(datadir: File,
|
|||
|
||||
implicit val ec = ExecutionContext.Implicits.global
|
||||
|
||||
val appConfig = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val config = appConfig.getConfig("eclair")
|
||||
val config = system.settings.config.getConfig("eclair")
|
||||
val chain = config.getString("chain")
|
||||
val keyManager = new LocalKeyManager(randomBytes(32), NodeParams.makeChainHash(chain))
|
||||
val database = db match {
|
||||
|
|
|
@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
|
|||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats}
|
||||
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
|
||||
import fr.acinq.eclair.io.{NodeURI, Peer}
|
||||
import fr.acinq.eclair.io.{NodeURI, Peer, PeerConnection}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
|
||||
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
|
||||
|
@ -41,7 +41,7 @@ import scodec.bits.ByteVector
|
|||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress])
|
||||
case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress])
|
||||
|
||||
case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed])
|
||||
|
||||
|
@ -127,8 +127,8 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
|||
private val externalIdMaxLength = 66
|
||||
|
||||
override def connect(target: Either[NodeURI, PublicKey])(implicit timeout: Timeout): Future[String] = target match {
|
||||
case Left(uri) => (appKit.switchboard ? Peer.Connect(uri)).mapTo[String]
|
||||
case Right(pubKey) => (appKit.switchboard ? Peer.Connect(pubKey, None)).mapTo[String]
|
||||
case Left(uri) => (appKit.switchboard ? Peer.Connect(uri)).mapTo[PeerConnection.ConnectionResult].map(_.toString)
|
||||
case Right(pubKey) => (appKit.switchboard ? Peer.Connect(pubKey, None)).mapTo[PeerConnection.ConnectionResult].map(_.toString)
|
||||
}
|
||||
|
||||
override def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] = {
|
||||
|
@ -204,7 +204,8 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
|||
override def newAddress(): Future[String] = Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
|
||||
|
||||
override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = {
|
||||
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse]
|
||||
val maxFee = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf).getMaxFee(amount)
|
||||
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, maxFee, assistedRoutes)).mapTo[RouteResponse]
|
||||
}
|
||||
|
||||
override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: Seq[PublicKey], trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = {
|
||||
|
@ -311,7 +312,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
|||
GetInfoResponse(
|
||||
version = Kit.getVersionLong,
|
||||
color = appKit.nodeParams.color.toString,
|
||||
features = appKit.nodeParams.features.toHex,
|
||||
features = appKit.nodeParams.features,
|
||||
nodeId = appKit.nodeParams.nodeId,
|
||||
alias = appKit.nodeParams.alias,
|
||||
chainHash = appKit.nodeParams.chainHash,
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package fr.acinq.eclair
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||
import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret}
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
|
||||
/**
|
||||
|
@ -26,23 +29,115 @@ sealed trait FeatureSupport
|
|||
|
||||
// @formatter:off
|
||||
object FeatureSupport {
|
||||
case object Mandatory extends FeatureSupport
|
||||
case object Optional extends FeatureSupport
|
||||
case object Mandatory extends FeatureSupport { override def toString: String = "mandatory" }
|
||||
case object Optional extends FeatureSupport { override def toString: String = "optional" }
|
||||
}
|
||||
|
||||
sealed trait Feature {
|
||||
|
||||
def rfcName: String
|
||||
def mandatory: Int
|
||||
def optional: Int = mandatory + 1
|
||||
|
||||
def supportBit(support: FeatureSupport): Int = support match {
|
||||
case FeatureSupport.Mandatory => mandatory
|
||||
case FeatureSupport.Optional => optional
|
||||
}
|
||||
|
||||
override def toString = rfcName
|
||||
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
sealed trait Feature {
|
||||
def rfcName: String
|
||||
case class ActivatedFeature(feature: Feature, support: FeatureSupport)
|
||||
|
||||
def mandatory: Int
|
||||
case class UnknownFeature(bitIndex: Int)
|
||||
|
||||
def optional: Int = mandatory + 1
|
||||
case class Features(activated: Set[ActivatedFeature], unknown: Set[UnknownFeature] = Set.empty) {
|
||||
|
||||
def hasFeature(feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match {
|
||||
case Some(s) => activated.contains(ActivatedFeature(feature, s))
|
||||
case None => hasFeature(feature, Some(Optional)) || hasFeature(feature, Some(Mandatory))
|
||||
}
|
||||
|
||||
def toByteVector: ByteVector = {
|
||||
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case ActivatedFeature(f, s) => f.supportBit(s) })
|
||||
val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex))
|
||||
val maxSize = activatedFeatureBytes.size.max(unknownFeatureBytes.size)
|
||||
activatedFeatureBytes.padLeft(maxSize) | unknownFeatureBytes.padLeft(maxSize)
|
||||
}
|
||||
|
||||
private def toByteVectorFromIndex(indexes: Set[Int]): ByteVector = {
|
||||
if (indexes.isEmpty) return ByteVector.empty
|
||||
// When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting feature bits.
|
||||
var buf = BitVector.fill(indexes.max + 1)(high = false).bytes.bits
|
||||
indexes.foreach { i =>
|
||||
buf = buf.set(i)
|
||||
}
|
||||
buf.reverse.bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask
|
||||
* off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue.
|
||||
* We use a long enough mask to account for future features.
|
||||
* TODO: remove that once eclair-mobile is patched.
|
||||
*/
|
||||
def maskFeaturesForEclairMobile(): Features = {
|
||||
Features(
|
||||
activated = activated.filter {
|
||||
case ActivatedFeature(PaymentSecret, _) => false
|
||||
case ActivatedFeature(BasicMultiPartPayment, _) => false
|
||||
case _ => true
|
||||
},
|
||||
unknown = unknown
|
||||
)
|
||||
}
|
||||
|
||||
override def toString = rfcName
|
||||
}
|
||||
|
||||
object Features {
|
||||
|
||||
def empty = Features(Set.empty[ActivatedFeature])
|
||||
|
||||
def apply(features: Set[ActivatedFeature]): Features = Features(activated = features)
|
||||
|
||||
def apply(bytes: ByteVector): Features = apply(bytes.bits)
|
||||
|
||||
def apply(bits: BitVector): Features = {
|
||||
val all = bits.toIndexedSeq.reverse.zipWithIndex.collect {
|
||||
case (true, idx) if knownFeatures.exists(_.optional == idx) => Right(ActivatedFeature(knownFeatures.find(_.optional == idx).get, Optional))
|
||||
case (true, idx) if knownFeatures.exists(_.mandatory == idx) => Right(ActivatedFeature(knownFeatures.find(_.mandatory == idx).get, Mandatory))
|
||||
case (true, idx) => Left(UnknownFeature(idx))
|
||||
}
|
||||
Features(
|
||||
activated = all.collect { case Right(af) => af }.toSet,
|
||||
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(ActivatedFeature(feature, support))
|
||||
case _ => None
|
||||
}
|
||||
})
|
||||
|
||||
/** 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)
|
||||
case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case object OptionDataLossProtect extends Feature {
|
||||
val rfcName = "option_data_loss_protect"
|
||||
val mandatory = 0
|
||||
|
@ -80,7 +175,7 @@ object Features {
|
|||
}
|
||||
|
||||
case object Wumbo extends Feature {
|
||||
val rfcName = "large_channel_support"
|
||||
val rfcName = "option_support_large_channel"
|
||||
val mandatory = 18
|
||||
}
|
||||
|
||||
|
@ -92,6 +187,28 @@ object Features {
|
|||
val mandatory = 50
|
||||
}
|
||||
|
||||
val knownFeatures: Set[Feature] = Set(
|
||||
OptionDataLossProtect,
|
||||
InitialRoutingSync,
|
||||
ChannelRangeQueries,
|
||||
VariableLengthOnion,
|
||||
ChannelRangeQueriesExtended,
|
||||
PaymentSecret,
|
||||
BasicMultiPartPayment,
|
||||
Wumbo,
|
||||
TrampolinePayment
|
||||
)
|
||||
|
||||
private val supportedMandatoryFeatures: Set[Feature] = Set(
|
||||
OptionDataLossProtect,
|
||||
ChannelRangeQueries,
|
||||
VariableLengthOnion,
|
||||
ChannelRangeQueriesExtended,
|
||||
PaymentSecret,
|
||||
BasicMultiPartPayment,
|
||||
Wumbo
|
||||
)
|
||||
|
||||
// Features may depend on other features, as specified in Bolt 9.
|
||||
private val featuresDependency = Map(
|
||||
ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil),
|
||||
|
@ -104,54 +221,20 @@ object Features {
|
|||
|
||||
case class FeatureException(message: String) extends IllegalArgumentException(message)
|
||||
|
||||
def validateFeatureGraph(features: BitVector): Option[FeatureException] = featuresDependency.collectFirst {
|
||||
case (feature, dependencies) if hasFeature(features, feature) && dependencies.exists(d => !hasFeature(features, d)) =>
|
||||
FeatureException(s"${features.toBin} sets $feature but is missing a dependency (${dependencies.filter(d => !hasFeature(features, d)).mkString(" and ")})")
|
||||
}
|
||||
|
||||
def validateFeatureGraph(features: ByteVector): Option[FeatureException] = validateFeatureGraph(features.bits)
|
||||
|
||||
// Note that BitVector indexes from left to right whereas the specification indexes from right to left.
|
||||
// This is why we have to reverse the bits to check if a feature is set.
|
||||
|
||||
private def hasFeature(features: BitVector, bit: Int): Boolean = features.sizeGreaterThan(bit) && features.reverse.get(bit)
|
||||
|
||||
def hasFeature(features: BitVector, feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match {
|
||||
case Some(FeatureSupport.Mandatory) => hasFeature(features, feature.mandatory)
|
||||
case Some(FeatureSupport.Optional) => hasFeature(features, feature.optional)
|
||||
case None => hasFeature(features, feature.optional) || hasFeature(features, feature.mandatory)
|
||||
}
|
||||
|
||||
def hasFeature(features: ByteVector, feature: Feature): Boolean = hasFeature(features.bits, feature)
|
||||
|
||||
def hasFeature(features: ByteVector, feature: Feature, support: Option[FeatureSupport]): Boolean = hasFeature(features.bits, feature, support)
|
||||
|
||||
/**
|
||||
* Check that the features that we understand are correctly specified, and that there are no mandatory features that
|
||||
* we don't understand (even bits).
|
||||
*/
|
||||
def areSupported(features: BitVector): Boolean = {
|
||||
val supportedMandatoryFeatures = Set(
|
||||
OptionDataLossProtect,
|
||||
ChannelRangeQueries,
|
||||
VariableLengthOnion,
|
||||
ChannelRangeQueriesExtended,
|
||||
PaymentSecret,
|
||||
BasicMultiPartPayment,
|
||||
Wumbo
|
||||
).map(_.mandatory.toLong)
|
||||
val reversed = features.reverse
|
||||
for (i <- 0L until reversed.length by 2) {
|
||||
if (reversed.get(i) && !supportedMandatoryFeatures.contains(i)) return false
|
||||
}
|
||||
|
||||
true
|
||||
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 ")})")
|
||||
}
|
||||
|
||||
/**
|
||||
* A feature set is supported if all even bits are supported.
|
||||
* We just ignore unknown odd bits.
|
||||
*/
|
||||
def areSupported(features: ByteVector): Boolean = areSupported(features.bits)
|
||||
def areSupported(features: Features): Boolean = {
|
||||
!features.unknown.exists(_.bitIndex % 2 == 0) && features.activated.forall {
|
||||
case ActivatedFeature(_, Optional) => true
|
||||
case ActivatedFeature(feature, Mandatory) => supportedMandatoryFeatures.contains(feature)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import akka.actor.ActorRef
|
|||
import com.google.common.net.HostAndPort
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, DeterministicWallet, OutPoint, Satoshi, Transaction, TxOut}
|
||||
import fr.acinq.eclair.Features.OptionDataLossProtect
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.payment.relay.Origin
|
||||
|
@ -26,6 +27,17 @@ object JsonSerializers {
|
|||
implicit val bytevector64ReadWriter: ReadWriter[ByteVector64] = readwriter[String].bimap[ByteVector64](_.bytes.toHex, s => ByteVector64.fromValidHex(s))
|
||||
implicit val uint64ReadWriter: ReadWriter[UInt64] = readwriter[String].bimap[UInt64](_.toString, s => UInt64(s.toLong))
|
||||
implicit val channelVersionReadWriter: ReadWriter[ChannelVersion] = readwriter[String].bimap[ChannelVersion](_.bits.toBin, s => ChannelVersion(BitVector.fromValidBin(s)))
|
||||
implicit val featureReadWriter: ReadWriter[Feature] = readwriter[String].bimap[Feature](
|
||||
_.rfcName,
|
||||
s => Features.knownFeatures.find(_.rfcName == s).getOrElse(OptionDataLossProtect)
|
||||
)
|
||||
implicit val feartureSupportReadWriter: ReadWriter[FeatureSupport] = readwriter[String].bimap(
|
||||
_.toString,
|
||||
s => if (s == "mandatory") FeatureSupport.Mandatory else FeatureSupport.Optional
|
||||
)
|
||||
implicit val activatedFeaturesReadWriter: ReadWriter[ActivatedFeature] = macroRW
|
||||
implicit val unknownFeaturesReadWriter: ReadWriter[UnknownFeature] = macroRW
|
||||
implicit val featuresReadWriter: ReadWriter[Features] = macroRW
|
||||
implicit val localParamsReadWriter: ReadWriter[LocalParams] = macroRW
|
||||
implicit val remoteParamsReadWriter: ReadWriter[RemoteParams] = macroRW
|
||||
implicit val onionRoutingPacketReadWriter: ReadWriter[OnionRoutingPacket] = macroRW
|
||||
|
@ -82,7 +94,7 @@ object JsonSerializers {
|
|||
implicit val upfrontShutdownScriptWriter: ReadWriter[UpfrontShutdownScript] = macroRW
|
||||
implicit val openChannelTlvWriter: ReadWriter[OpenChannelTlv] = macroRW
|
||||
implicit val acceptChannelTlvWriter: ReadWriter[AcceptChannelTlv] = macroRW
|
||||
implicit val initReadWriter: ReadWriter[Init] = readwriter[ByteVector].bimap[Init](_.features, s => Init(s))
|
||||
implicit val initReadWriter: ReadWriter[Init] = readwriter[Features].bimap[Init](_.features, s => Init(s))
|
||||
implicit val openChannelReadWriter: ReadWriter[OpenChannel] = macroRW
|
||||
implicit val acceptChannelReadWriter: ReadWriter[AcceptChannel] = macroRW
|
||||
implicit val fundingCreatedReadWriter: ReadWriter[FundingCreated] = macroRW
|
||||
|
|
|
@ -22,8 +22,8 @@ import java.sql.DriverManager
|
|||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
import com.typesafe.config.{Config, ConfigFactory, ConfigValueType}
|
||||
import com.google.common.io.Files
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi}
|
||||
import fr.acinq.eclair.NodeParams.WatcherType
|
||||
|
@ -48,8 +48,8 @@ case class NodeParams(keyManager: KeyManager,
|
|||
alias: String,
|
||||
color: Color,
|
||||
publicAddresses: List[NodeAddress],
|
||||
features: ByteVector,
|
||||
overrideFeatures: Map[PublicKey, ByteVector],
|
||||
features: Features,
|
||||
overrideFeatures: Map[PublicKey, Features],
|
||||
syncWhitelist: Set[PublicKey],
|
||||
dustLimit: Satoshi,
|
||||
onChainFeeConf: OnChainFeeConf,
|
||||
|
@ -107,10 +107,9 @@ object NodeParams {
|
|||
* 3) Optionally provided config
|
||||
* 4) Default values in reference.conf
|
||||
*/
|
||||
def loadConfiguration(datadir: File, overrideDefaults: Config = ConfigFactory.empty()) =
|
||||
def loadConfiguration(datadir: File) =
|
||||
ConfigFactory.parseProperties(System.getProperties)
|
||||
.withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf")))
|
||||
.withFallback(overrideDefaults)
|
||||
.withFallback(ConfigFactory.load())
|
||||
|
||||
def getSeed(datadir: File): ByteVector = {
|
||||
|
@ -149,6 +148,10 @@ object NodeParams {
|
|||
case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'")
|
||||
}
|
||||
|
||||
// since v0.4.1 features cannot be a byte vector (hex string)
|
||||
val isFeatureByteVector = config.getValue("features").valueType() == ConfigValueType.STRING
|
||||
require(!isFeatureByteVector, "configuration key 'features' have moved from bytevector to human readable (ex: 'feature-name' = optional/mandatory)")
|
||||
|
||||
val chain = config.getString("chain")
|
||||
val chainHash = makeChainHash(chain)
|
||||
|
||||
|
@ -182,13 +185,13 @@ object NodeParams {
|
|||
val nodeAlias = config.getString("node-alias")
|
||||
require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)")
|
||||
|
||||
val features = ByteVector.fromValidHex(config.getString("features"))
|
||||
val features = Features.fromConfiguration(config)
|
||||
val featuresErr = Features.validateFeatureGraph(features)
|
||||
require(featuresErr.isEmpty, featuresErr.map(_.message))
|
||||
|
||||
val overrideFeatures: Map[PublicKey, ByteVector] = config.getConfigList("override-features").asScala.map { e =>
|
||||
val overrideFeatures: Map[PublicKey, Features] = config.getConfigList("override-features").asScala.map { e =>
|
||||
val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid")))
|
||||
val f = ByteVector.fromValidHex(e.getString("features"))
|
||||
val f = Features.fromConfiguration(e)
|
||||
p -> f
|
||||
}.toMap
|
||||
|
||||
|
|
|
@ -57,12 +57,10 @@ import scala.util.{Failure, Success}
|
|||
* Created by PM on 25/01/2016.
|
||||
*
|
||||
* @param datadir directory where eclair-core will write/read its data.
|
||||
* @param overrideDefaults use this parameter to programmatically override the node configuration .
|
||||
* @param seed_opt optional seed, if set eclair will use it instead of generating one and won't create a seed.dat file.
|
||||
* @param db optional databases to use, if not set eclair will create the necessary databases
|
||||
*/
|
||||
class Setup(datadir: File,
|
||||
overrideDefaults: Config = ConfigFactory.empty(),
|
||||
seed_opt: Option[ByteVector] = None,
|
||||
db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging {
|
||||
|
||||
|
@ -77,8 +75,7 @@ class Setup(datadir: File,
|
|||
|
||||
|
||||
datadir.mkdirs()
|
||||
val appConfig = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val config = appConfig.getConfig("eclair")
|
||||
val config = system.settings.config.getConfig("eclair")
|
||||
val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir))
|
||||
val chain = config.getString("chain")
|
||||
val chaindir = new File(datadir, chain)
|
||||
|
|
|
@ -50,8 +50,7 @@ class SyncLiteSetup(datadir: File,
|
|||
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
|
||||
logger.info(s"datadir=${datadir.getCanonicalPath}")
|
||||
|
||||
val appConfig = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val config = appConfig.getConfig("eclair")
|
||||
val config = system.settings.config.getConfig("eclair")
|
||||
val chain = config.getString("chain")
|
||||
val keyManager = new LocalKeyManager(randomBytes32, NodeParams.makeChainHash(chain))
|
||||
val database = db match {
|
||||
|
|
|
@ -24,7 +24,7 @@ import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, T
|
|||
import fr.acinq.eclair.transactions.CommitmentSpec
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64}
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
|
||||
/**
|
||||
|
@ -230,7 +230,7 @@ final case class LocalParams(nodeId: PublicKey,
|
|||
maxAcceptedHtlcs: Int,
|
||||
isFunder: Boolean,
|
||||
defaultFinalScriptPubKey: ByteVector,
|
||||
features: ByteVector)
|
||||
features: Features)
|
||||
|
||||
final case class RemoteParams(nodeId: PublicKey,
|
||||
dustLimit: Satoshi,
|
||||
|
@ -244,7 +244,7 @@ final case class RemoteParams(nodeId: PublicKey,
|
|||
paymentBasepoint: PublicKey,
|
||||
delayedPaymentBasepoint: PublicKey,
|
||||
htlcBasepoint: PublicKey,
|
||||
features: ByteVector)
|
||||
features: Features)
|
||||
|
||||
object ChannelFlags {
|
||||
val AnnounceChannel = 0x01.toByte
|
||||
|
|
|
@ -20,7 +20,6 @@ import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256}
|
||||
import fr.acinq.bitcoin.Script._
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair.Features.{Wumbo, hasFeature}
|
||||
import fr.acinq.eclair.blockchain.EclairWallet
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets}
|
||||
import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL
|
||||
|
@ -34,7 +33,6 @@ import fr.acinq.eclair.wire._
|
|||
import fr.acinq.eclair.{NodeParams, ShortChannelId, addressToPublicKeyScript, _}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.compat.Platform
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
@ -105,7 +103,7 @@ object Helpers {
|
|||
if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis > nodeParams.maxFundingSatoshis) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, nodeParams.maxFundingSatoshis)
|
||||
|
||||
// BOLT #2: Channel funding limits
|
||||
if (open.fundingSatoshis >= Channel.MAX_FUNDING && !hasFeature(nodeParams.features, Wumbo)) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING)
|
||||
if (open.fundingSatoshis >= Channel.MAX_FUNDING && !nodeParams.features.hasFeature(Features.Wumbo)) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING)
|
||||
|
||||
// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
|
||||
if (open.pushMsat > open.fundingSatoshis) throw InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi)
|
||||
|
@ -219,7 +217,7 @@ object Helpers {
|
|||
}
|
||||
|
||||
def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = {
|
||||
val features = ByteVector.empty // empty features for now
|
||||
val features = Features.empty // empty features for now
|
||||
val fundingPubKey = nodeParams.keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)
|
||||
val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey.path, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features)
|
||||
AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig)
|
||||
|
|
|
@ -22,10 +22,9 @@ import java.nio.ByteOrder
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, Protocol}
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.{Features, ShortChannelId}
|
||||
import fr.acinq.eclair.channel.{ChannelVersion, LocalParams}
|
||||
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
trait KeyManager {
|
||||
def nodeKey: DeterministicWallet.ExtendedPrivateKey
|
||||
|
@ -106,7 +105,7 @@ trait KeyManager {
|
|||
* @return a (nodeSig, bitcoinSig) pair. nodeSig is the signature of the channel announcement with our node's
|
||||
* private key, bitcoinSig is the signature of the channel announcement with our funding private key
|
||||
*/
|
||||
def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64)
|
||||
def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64)
|
||||
}
|
||||
|
||||
object KeyManager {
|
||||
|
|
|
@ -23,7 +23,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Deterministi
|
|||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
|
||||
import fr.acinq.eclair.{ShortChannelId, secureRandom}
|
||||
import fr.acinq.eclair.{Features, ShortChannelId, secureRandom}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
object LocalKeyManager {
|
||||
|
@ -143,7 +143,7 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana
|
|||
Transactions.sign(tx, currentKey)
|
||||
}
|
||||
|
||||
override def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = {
|
||||
override def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64) = {
|
||||
val localNodeSecret = nodeKey.privateKey
|
||||
val localFundingPrivKey = privateKeys.get(fundingKeyPath).privateKey
|
||||
Announcements.signChannelAnnouncement(chainHash, shortChannelId, localNodeSecret, remoteNodeId, localFundingPrivKey, remoteFundingKey, features)
|
||||
|
|
|
@ -269,7 +269,10 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co
|
|||
}
|
||||
}
|
||||
|
||||
override def aroundPostStop(): Unit = connection ! Tcp.Close // attempts to gracefully close the connection when dying
|
||||
onTermination {
|
||||
case _: StopEvent =>
|
||||
connection ! Tcp.Close // attempts to gracefully close the connection when dying
|
||||
}
|
||||
|
||||
initialize()
|
||||
|
||||
|
|
|
@ -64,14 +64,14 @@ class BackupHandler private(databases: Databases, backupFile: File, backupScript
|
|||
// publish a notification that we have updated our backup
|
||||
context.system.eventStream.publish(BackupCompleted)
|
||||
|
||||
log.info(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms")
|
||||
log.debug(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms")
|
||||
|
||||
backupScript_opt.foreach(backupScript => {
|
||||
Try {
|
||||
// run the script in the current thread and wait until it terminates
|
||||
Process(backupScript).!
|
||||
} match {
|
||||
case Success(exitCode) => log.info(s"backup notify script $backupScript returned $exitCode")
|
||||
case Success(exitCode) => log.debug(s"backup notify script $backupScript returned $exitCode")
|
||||
case Failure(cause) => log.warning(s"cannot start backup notify script $backupScript: $cause")
|
||||
}
|
||||
})
|
||||
|
|
|
@ -20,7 +20,7 @@ import java.sql.Connection
|
|||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi}
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.{Features, ShortChannelId}
|
||||
import fr.acinq.eclair.db.NetworkDb
|
||||
import fr.acinq.eclair.router.Router.PublicChannel
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec}
|
||||
|
@ -43,7 +43,7 @@ class SqliteNetworkDb(sqlite: Connection, chainHash: ByteVector32) extends Netwo
|
|||
|
||||
// on Android we prune as many fields as possible to save memory
|
||||
val channelAnnouncementWitnessCodec =
|
||||
("features" | provide(null.asInstanceOf[ByteVector])) ::
|
||||
("features" | provide(null.asInstanceOf[Features])) ::
|
||||
("chainHash" | provide(null.asInstanceOf[ByteVector32])) ::
|
||||
("shortChannelId" | shortchannelid) ::
|
||||
("nodeId1" | publicKey) ::
|
||||
|
|
|
@ -24,7 +24,6 @@ import akka.io.Tcp.SO.KeepAlive
|
|||
import akka.io.{IO, Tcp}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.Logs.LogCategory
|
||||
import fr.acinq.eclair.io.Client.ConnectionFailed
|
||||
import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected, Socks5Error}
|
||||
import fr.acinq.eclair.tor.{Socks5Connection, Socks5ProxyParams}
|
||||
import fr.acinq.eclair.{Logs, NodeParams}
|
||||
|
@ -60,7 +59,7 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re
|
|||
case Tcp.CommandFailed(c: Tcp.Connect) =>
|
||||
val peerOrProxyAddress = c.remoteAddress
|
||||
log.info(s"connection failed to ${str(peerOrProxyAddress)}")
|
||||
origin_opt.foreach(_ ! Status.Failure(ConnectionFailed(remoteAddress)))
|
||||
origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress))
|
||||
context stop self
|
||||
|
||||
case Tcp.Connected(peerOrProxyAddress, _) =>
|
||||
|
@ -75,24 +74,28 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re
|
|||
context become {
|
||||
case Tcp.CommandFailed(_: Socks5Connect) =>
|
||||
log.info(s"connection failed to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}")
|
||||
origin_opt.foreach(_ ! Status.Failure(ConnectionFailed(remoteAddress)))
|
||||
origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress))
|
||||
context stop self
|
||||
case Socks5Connected(_) =>
|
||||
log.info(s"connected to ${str(remoteAddress)} via SOCKS5 proxy ${str(proxyAddress)}")
|
||||
auth(proxy)
|
||||
context become connected(proxy)
|
||||
context unwatch proxy
|
||||
val peerConnection = auth(proxy)
|
||||
context watch peerConnection
|
||||
context become connected(peerConnection)
|
||||
case Terminated(actor) if actor == proxy =>
|
||||
context stop self
|
||||
}
|
||||
case None =>
|
||||
val peerAddress = peerOrProxyAddress
|
||||
log.info(s"connected to ${str(peerAddress)}")
|
||||
auth(connection)
|
||||
context watch connection
|
||||
context become connected(connection)
|
||||
val peerConnection = auth(connection)
|
||||
context watch peerConnection
|
||||
context become connected(peerConnection)
|
||||
}
|
||||
}
|
||||
|
||||
def connected(connection: ActorRef): Receive = {
|
||||
case Terminated(actor) if actor == connection =>
|
||||
def connected(peerConnection: ActorRef): Receive = {
|
||||
case Terminated(actor) if actor == peerConnection =>
|
||||
context stop self
|
||||
}
|
||||
|
||||
|
@ -100,7 +103,7 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re
|
|||
log.warning(s"unhandled message=$message")
|
||||
}
|
||||
|
||||
// we should not restart a failing socks client
|
||||
// we should not restart a failing socks client or transport handler
|
||||
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = false) {
|
||||
case t =>
|
||||
Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = Some(remoteNodeId))) {
|
||||
|
@ -116,13 +119,14 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re
|
|||
|
||||
private def str(address: InetSocketAddress): String = s"${address.getHostString}:${address.getPort}"
|
||||
|
||||
def auth(connection: ActorRef) = {
|
||||
def auth(connection: ActorRef): ActorRef = {
|
||||
val peerConnection = context.actorOf(PeerConnection.props(
|
||||
nodeParams = nodeParams,
|
||||
switchboard = switchboard,
|
||||
router = router
|
||||
))
|
||||
peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt)
|
||||
peerConnection
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,6 +134,4 @@ object Client {
|
|||
|
||||
def props(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, switchboard, router, address, remoteNodeId, origin_opt))
|
||||
|
||||
case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address")
|
||||
|
||||
}
|
||||
|
|
|
@ -66,22 +66,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe
|
|||
reconnectionTask forward p
|
||||
stay
|
||||
|
||||
case Event(PeerConnection.ConnectionReady(peerConnection, remoteNodeId1, address, outgoing, localInit, remoteInit), d: DisconnectedData) =>
|
||||
require(remoteNodeId == remoteNodeId1, s"invalid nodeid: $remoteNodeId != $remoteNodeId1")
|
||||
log.debug("got authenticated connection to address {}:{}", address.getHostString, address.getPort)
|
||||
|
||||
context watch peerConnection
|
||||
|
||||
if (outgoing) {
|
||||
// we store the node address upon successful outgoing connection, so we can reconnect later
|
||||
// any previous address is overwritten
|
||||
NodeAddress.fromParts(address.getHostString, address.getPort).map(nodeAddress => nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, nodeAddress))
|
||||
}
|
||||
|
||||
// let's bring existing/requested channels online
|
||||
d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(peerConnection, localInit, remoteInit)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id)
|
||||
|
||||
goto(CONNECTED) using ConnectedData(address, peerConnection, localInit, remoteInit, d.channels.map { case (k: ChannelId, v) => (k, v) })
|
||||
case Event(connectionReady: PeerConnection.ConnectionReady, d: DisconnectedData) =>
|
||||
gotoConnected(connectionReady, d.channels.map { case (k: ChannelId, v) => (k, v) })
|
||||
|
||||
case Event(Terminated(actor), d: DisconnectedData) if d.channels.exists(_._2 == actor) =>
|
||||
val h = d.channels.filter(_._2 == actor).keys
|
||||
|
@ -100,7 +86,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe
|
|||
when(CONNECTED) {
|
||||
dropStaleMessages {
|
||||
case Event(_: Peer.Connect, _) =>
|
||||
sender ! "already connected"
|
||||
sender ! PeerConnection.ConnectionResult.AlreadyConnected
|
||||
stay
|
||||
|
||||
case Event(Channel.OutgoingMessage(msg, peerConnection), d: ConnectedData) if peerConnection == d.peerConnection => // this is an outgoing message, but we need to make sure that this is for the current active connection
|
||||
|
@ -123,10 +109,10 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe
|
|||
stay
|
||||
|
||||
case Event(c: Peer.OpenChannel, d: ConnectedData) =>
|
||||
if (c.fundingSatoshis >= Channel.MAX_FUNDING && !Features.hasFeature(nodeParams.features, Wumbo)) {
|
||||
if (c.fundingSatoshis >= Channel.MAX_FUNDING && !nodeParams.features.hasFeature(Wumbo)) {
|
||||
sender ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)"))
|
||||
stay
|
||||
} else if (c.fundingSatoshis >= Channel.MAX_FUNDING && !Features.hasFeature(d.remoteInit.features, Wumbo)) {
|
||||
} else if (c.fundingSatoshis >= Channel.MAX_FUNDING && !d.remoteInit.features.hasFeature(Wumbo)) {
|
||||
sender ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, the remote peer doesn't support wumbo"))
|
||||
stay
|
||||
} else if (c.fundingSatoshis > nodeParams.maxFundingSatoshis) {
|
||||
|
@ -210,8 +196,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe
|
|||
context unwatch d.peerConnection
|
||||
d.peerConnection ! PoisonPill
|
||||
d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id)
|
||||
self forward connectionReady // we preserve the origin
|
||||
goto(DISCONNECTED) using DisconnectedData(d.channels.collect { case (k: FinalChannelId, v) => (k, v) })
|
||||
gotoConnected(connectionReady, d.channels)
|
||||
|
||||
case Event(unhandledMsg: LightningMessage, _) =>
|
||||
log.warning("ignoring message {}", unhandledMsg)
|
||||
|
@ -241,9 +226,11 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe
|
|||
}
|
||||
|
||||
onTransition {
|
||||
case _ -> CONNECTED =>
|
||||
case DISCONNECTED -> CONNECTED =>
|
||||
Metrics.PeersConnected.withoutTags().increment()
|
||||
context.system.eventStream.publish(PeerConnected(self, remoteNodeId))
|
||||
case CONNECTED -> CONNECTED => // connection switch
|
||||
context.system.eventStream.publish(PeerConnected(self, remoteNodeId))
|
||||
case CONNECTED -> DISCONNECTED =>
|
||||
Metrics.PeersConnected.withoutTags().decrement()
|
||||
context.system.eventStream.publish(PeerDisconnected(self, remoteNodeId))
|
||||
|
@ -256,6 +243,24 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe
|
|||
context.system.eventStream.publish(PeerDisconnected(self, remoteNodeId))
|
||||
}
|
||||
|
||||
def gotoConnected(connectionReady: PeerConnection.ConnectionReady, channels: Map[ChannelId, ActorRef]): State = {
|
||||
require(remoteNodeId == connectionReady.remoteNodeId, s"invalid nodeid: $remoteNodeId != ${connectionReady.remoteNodeId}")
|
||||
log.debug("got authenticated connection to address {}:{}", connectionReady.address.getHostString, connectionReady.address.getPort)
|
||||
|
||||
context watch connectionReady.peerConnection
|
||||
|
||||
if (connectionReady.outgoing) {
|
||||
// we store the node address upon successful outgoing connection, so we can reconnect later
|
||||
// any previous address is overwritten
|
||||
NodeAddress.fromParts(connectionReady.address.getHostString, connectionReady.address.getPort).map(nodeAddress => nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, nodeAddress))
|
||||
}
|
||||
|
||||
// let's bring existing/requested channels online
|
||||
channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(connectionReady.peerConnection, connectionReady.localInit, connectionReady.remoteInit)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id)
|
||||
|
||||
goto(CONNECTED) using ConnectedData(connectionReady.address, connectionReady.peerConnection, connectionReady.localInit, connectionReady.remoteInit, channels)
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to ignore [[LightningMessage]] not sent by the current [[PeerConnection]]. This may happen if we switch
|
||||
* between connections.
|
||||
|
|
|
@ -30,7 +30,7 @@ import fr.acinq.eclair.router.Router._
|
|||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{wire, _}
|
||||
import scodec.Attempt
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
@ -89,8 +89,9 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
|
|||
switchboard ! Authenticated(self, remoteNodeId)
|
||||
goto(BEFORE_INIT) using BeforeInitData(remoteNodeId, d.pendingAuth, d.transport)
|
||||
|
||||
case Event(AuthTimeout, _) =>
|
||||
case Event(AuthTimeout, d: AuthenticatingData) =>
|
||||
log.warning(s"authentication timed out after ${nodeParams.authTimeout}")
|
||||
d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.AuthenticationFailed("authentication timed out"))
|
||||
stop(FSM.Normal)
|
||||
}
|
||||
|
||||
|
@ -100,23 +101,9 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
|
|||
Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment()
|
||||
val localFeatures = nodeParams.overrideFeatures.get(d.remoteNodeId) match {
|
||||
case Some(f) => f
|
||||
case None =>
|
||||
// Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask
|
||||
// off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue.
|
||||
// We use a long enough mask to account for future features.
|
||||
// TODO: remove that once eclair-mobile is patched.
|
||||
val tweakedFeatures = BitVector.bits(nodeParams.features.bits.reverse.toIndexedSeq.zipWithIndex.map {
|
||||
// we disable those bits if they are set...
|
||||
case (true, 14) => false
|
||||
case (true, 15) => false
|
||||
case (true, 16) => false
|
||||
case (true, 17) => false
|
||||
// ... and leave the others untouched
|
||||
case (value, _) => value
|
||||
}).reverse.bytes.dropWhile(_ == 0)
|
||||
tweakedFeatures
|
||||
case None => nodeParams.features.maskFeaturesForEclairMobile()
|
||||
}
|
||||
log.info(s"using features=${localFeatures.toBin}")
|
||||
log.info(s"using features=$localFeatures")
|
||||
val localInit = wire.Init(localFeatures, TlvStream(InitTlv.Networks(nodeParams.chainHash :: Nil)))
|
||||
d.transport ! localInit
|
||||
setTimer(INIT_TIMER, InitTimeout, nodeParams.initTimeout)
|
||||
|
@ -129,27 +116,27 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
|
|||
cancelTimer(INIT_TIMER)
|
||||
d.transport ! TransportHandler.ReadAck(remoteInit)
|
||||
|
||||
log.info(s"peer is using features=${remoteInit.features.toBin}, networks=${remoteInit.networks.mkString(",")}")
|
||||
log.info(s"peer is using features=${remoteInit.features}, networks=${remoteInit.networks.mkString(",")}")
|
||||
|
||||
if (remoteInit.networks.nonEmpty && !remoteInit.networks.contains(d.nodeParams.chainHash)) {
|
||||
log.warning(s"incompatible networks (${remoteInit.networks}), disconnecting")
|
||||
d.pendingAuth.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible networks")))
|
||||
d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("incompatible networks"))
|
||||
d.transport ! PoisonPill
|
||||
stay
|
||||
} else if (!Features.areSupported(remoteInit.features)) {
|
||||
log.warning("incompatible features, disconnecting")
|
||||
d.pendingAuth.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible features")))
|
||||
d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("incompatible features"))
|
||||
d.transport ! PoisonPill
|
||||
stay
|
||||
} else {
|
||||
Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initialized).increment()
|
||||
d.peer ! ConnectionReady(self, d.remoteNodeId, d.pendingAuth.address, d.pendingAuth.outgoing, d.localInit, remoteInit)
|
||||
|
||||
d.pendingAuth.origin_opt.foreach(origin => origin ! "connected")
|
||||
d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.Connected)
|
||||
|
||||
def localHasFeature(f: Feature): Boolean = Features.hasFeature(d.localInit.features, f)
|
||||
def localHasFeature(f: Feature): Boolean = d.localInit.features.hasFeature(f)
|
||||
|
||||
def remoteHasFeature(f: Feature): Boolean = Features.hasFeature(remoteInit.features, f)
|
||||
def remoteHasFeature(f: Feature): Boolean = remoteInit.features.hasFeature(f)
|
||||
|
||||
val canUseChannelRangeQueries = localHasFeature(Features.ChannelRangeQueries) && remoteHasFeature(Features.ChannelRangeQueries)
|
||||
val canUseChannelRangeQueriesEx = localHasFeature(Features.ChannelRangeQueriesExtended) && remoteHasFeature(Features.ChannelRangeQueriesExtended)
|
||||
|
@ -177,8 +164,9 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
|
|||
goto(CONNECTED) using ConnectedData(d.nodeParams, d.remoteNodeId, d.transport, d.peer, d.localInit, remoteInit, rebroadcastDelay)
|
||||
}
|
||||
|
||||
case Event(InitTimeout, _) =>
|
||||
case Event(InitTimeout, d: InitializingData) =>
|
||||
log.warning(s"initialization timed out after ${nodeParams.initTimeout}")
|
||||
d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("initialization timed out"))
|
||||
stop(FSM.Normal)
|
||||
}
|
||||
}
|
||||
|
@ -382,6 +370,12 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
|
|||
Logs.withMdc(diagLog)(Logs.mdc(category_opt = Some(Logs.LogCategory.CONNECTION))) {
|
||||
log.info("transport died, stopping")
|
||||
}
|
||||
d match {
|
||||
case a: AuthenticatingData => a.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.AuthenticationFailed("connection aborted while authenticating"))
|
||||
case a: BeforeInitData => a.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("connection aborted while initializing"))
|
||||
case a: InitializingData => a.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("connection aborted while initializing"))
|
||||
case _ => ()
|
||||
}
|
||||
stop(FSM.Normal)
|
||||
|
||||
case Event(_: GossipDecision.Accepted, _) => stay // for now we don't do anything with those events
|
||||
|
@ -500,6 +494,19 @@ object PeerConnection {
|
|||
case class InitializeConnection(peer: ActorRef)
|
||||
case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, localInit: wire.Init, remoteInit: wire.Init)
|
||||
|
||||
sealed trait ConnectionResult
|
||||
object ConnectionResult {
|
||||
sealed trait Success extends ConnectionResult
|
||||
sealed trait Failure extends ConnectionResult
|
||||
|
||||
case object NoAddressFound extends ConnectionResult.Failure { override def toString: String = "no address found" }
|
||||
case class ConnectionFailed(address: InetSocketAddress) extends ConnectionResult.Failure { override def toString: String = s"connection failed to $address" }
|
||||
case class AuthenticationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason }
|
||||
case class InitializationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason }
|
||||
case object AlreadyConnected extends ConnectionResult.Failure { override def toString: String = "already connected" }
|
||||
case object Connected extends ConnectionResult.Success { override def toString: String = "connected" }
|
||||
}
|
||||
|
||||
case class DelayedRebroadcast(rebroadcast: Rebroadcast)
|
||||
|
||||
case class Behavior(fundingTxAlreadySpentCount: Int = 0, ignoreNetworkAnnouncement: Boolean = false)
|
||||
|
|
|
@ -50,7 +50,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends
|
|||
startWith(IDLE, IdleData(Nothing))
|
||||
|
||||
when(CONNECTING) {
|
||||
case Event(Status.Failure(_: Client.ConnectionFailed), d: ConnectingData) =>
|
||||
case Event(_: PeerConnection.ConnectionResult.ConnectionFailed, d: ConnectingData) =>
|
||||
log.info(s"connection failed, next reconnection in ${d.nextReconnectionDelay.toSeconds} seconds")
|
||||
setReconnectTimer(d.nextReconnectionDelay)
|
||||
goto(WAITING) using WaitingData(nextReconnectionDelay(d.nextReconnectionDelay, nodeParams.maxReconnectInterval))
|
||||
|
@ -121,9 +121,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends
|
|||
}
|
||||
|
||||
whenUnhandled {
|
||||
case Event("connected", _) => stay
|
||||
|
||||
case Event(Status.Failure(_: Client.ConnectionFailed), _) => stay
|
||||
case Event(_: PeerConnection.ConnectionResult, _) => stay
|
||||
|
||||
case Event(TickReconnect, _) => stay
|
||||
|
||||
|
@ -135,7 +133,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends
|
|||
.map(hostAndPort2InetSocketAddress)
|
||||
.orElse(getPeerAddressFromDb(nodeParams.db.peers, nodeParams.db.network, remoteNodeId)) match {
|
||||
case Some(address) => connect(address, origin = sender)
|
||||
case None => sender ! "no address found"
|
||||
case None => sender ! PeerConnection.ConnectionResult.NoAddressFound
|
||||
}
|
||||
stay
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package fr.acinq.eclair.payment
|
|||
import akka.event.LoggingAdapter
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.eclair.Features.VariableLengthOnion
|
||||
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop}
|
||||
|
@ -54,11 +55,11 @@ object IncomingPacket {
|
|||
|
||||
case class DecodedOnionPacket[T <: Onion.PacketType](payload: T, next: OnionRoutingPacket)
|
||||
|
||||
private def decryptOnion[T <: Onion.PacketType : ClassTag](add: UpdateAddHtlc, privateKey: PrivateKey, features: ByteVector)(packet: OnionRoutingPacket, packetType: Sphinx.OnionRoutingPacket[T])(implicit log: LoggingAdapter): Either[FailureMessage, DecodedOnionPacket[T]] =
|
||||
private def decryptOnion[T <: Onion.PacketType : ClassTag](add: UpdateAddHtlc, privateKey: PrivateKey, features: Features)(packet: OnionRoutingPacket, packetType: Sphinx.OnionRoutingPacket[T])(implicit log: LoggingAdapter): Either[FailureMessage, DecodedOnionPacket[T]] =
|
||||
packetType.peel(privateKey, add.paymentHash, packet) match {
|
||||
case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) =>
|
||||
OnionCodecs.perHopPayloadCodecByPacketType(packetType, p.isLastPacket).decode(payload.bits) match {
|
||||
case Attempt.Successful(DecodeResult(_: Onion.TlvFormat, _)) if !Features.hasFeature(features, Features.VariableLengthOnion) => Left(InvalidRealm)
|
||||
case Attempt.Successful(DecodeResult(_: Onion.TlvFormat, _)) if !features.hasFeature(VariableLengthOnion) => Left(InvalidRealm)
|
||||
case Attempt.Successful(DecodeResult(perHopPayload: T, remainder)) =>
|
||||
if (remainder.nonEmpty) {
|
||||
log.warning(s"${remainder.length} bits remaining after per-hop payload decoding: there might be an issue with the onion codec")
|
||||
|
@ -84,7 +85,7 @@ object IncomingPacket {
|
|||
* @param features this node's supported features
|
||||
* @return whether the payment is to be relayed or if our node is the final recipient (or an error).
|
||||
*/
|
||||
def decrypt(add: UpdateAddHtlc, privateKey: PrivateKey, features: ByteVector)(implicit log: LoggingAdapter): Either[FailureMessage, IncomingPacket] = {
|
||||
def decrypt(add: UpdateAddHtlc, privateKey: PrivateKey, features: Features)(implicit log: LoggingAdapter): Either[FailureMessage, IncomingPacket] = {
|
||||
decryptOnion(add, privateKey, features)(add.onionRoutingPacket, Sphinx.PaymentPacket) match {
|
||||
case Left(failure) => Left(failure)
|
||||
// NB: we don't validate the ChannelRelayPacket here because its fees and cltv depend on what channel we'll choose to use.
|
||||
|
|
|
@ -18,14 +18,12 @@ 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.Features.{PaymentSecret => PaymentSecretF, _}
|
||||
import fr.acinq.eclair.payment.PaymentRequest._
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32}
|
||||
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
|
||||
import scodec.codecs.{list, ubyte}
|
||||
import scodec.{Codec, Err}
|
||||
|
||||
import scala.compat.Platform
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Try
|
||||
|
||||
|
@ -86,7 +84,7 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
|
|||
case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta
|
||||
}
|
||||
|
||||
lazy val features: Features = tags.collectFirst { case f: Features => f }.getOrElse(Features(BitVector.empty))
|
||||
lazy val features: PaymentRequestFeatures = tags.collectFirst { case f: PaymentRequestFeatures => f }.getOrElse(PaymentRequestFeatures(BitVector.empty))
|
||||
|
||||
def isExpired: Boolean = expiry match {
|
||||
case Some(expiryTime) => timestamp + expiryTime <= System.currentTimeMillis.milliseconds.toSeconds
|
||||
|
@ -129,7 +127,7 @@ object PaymentRequest {
|
|||
def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], paymentHash: ByteVector32, privateKey: PrivateKey,
|
||||
description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
|
||||
extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L,
|
||||
features: Option[Features] = Some(Features(VariableLengthOnion.optional, PaymentSecretF.optional))): PaymentRequest = {
|
||||
features: Option[PaymentRequestFeatures] = Some(PaymentRequestFeatures(Features.VariableLengthOnion.optional, Features.PaymentSecret.optional))): PaymentRequest = {
|
||||
|
||||
val prefix = prefixes(chainHash)
|
||||
val tags = {
|
||||
|
@ -331,25 +329,21 @@ object PaymentRequest {
|
|||
/**
|
||||
* Features supported or required for receiving this payment.
|
||||
*/
|
||||
case class Features(bitmask: BitVector) extends TaggedField {
|
||||
lazy val supported: Boolean = areSupported(bitmask)
|
||||
lazy val allowMultiPart: Boolean = hasFeature(bitmask, BasicMultiPartPayment)
|
||||
lazy val allowPaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF)
|
||||
lazy val requirePaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF, Some(FeatureSupport.Mandatory))
|
||||
lazy val allowTrampoline: Boolean = hasFeature(bitmask, TrampolinePayment)
|
||||
case class PaymentRequestFeatures(bitmask: BitVector) extends TaggedField {
|
||||
lazy val features: Features = Features(bitmask)
|
||||
lazy val supported: Boolean = Features.areSupported(features)
|
||||
lazy val allowMultiPart: Boolean = features.hasFeature(Features.BasicMultiPartPayment)
|
||||
lazy val allowPaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret)
|
||||
lazy val requirePaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory))
|
||||
lazy val allowTrampoline: Boolean = features.hasFeature(Features.TrampolinePayment)
|
||||
|
||||
def toByteVector: ByteVector = features.toByteVector
|
||||
|
||||
override def toString: String = s"Features(${bitmask.toBin})"
|
||||
|
||||
// When converting from BitVector to ByteVector, scodec pads right instead of left so we have to do this ourselves.
|
||||
// We also want to enforce a minimal encoding of the feature bytes.
|
||||
def toByteVector: ByteVector = {
|
||||
val pad = if (bitmask.length % 8 == 0) 0 else 8 - bitmask.length % 8
|
||||
bitmask.padLeft(bitmask.length + pad).bytes.dropWhile(_ == 0)
|
||||
}
|
||||
}
|
||||
|
||||
object Features {
|
||||
def apply(features: Int*): Features = Features(long2bits(features.foldLeft(0L) {
|
||||
object PaymentRequestFeatures {
|
||||
def apply(features: Int*): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) {
|
||||
case (current, feature) => current + (1L << feature)
|
||||
}))
|
||||
}
|
||||
|
@ -395,7 +389,7 @@ object PaymentRequest {
|
|||
.typecase(2, dataCodec(bits).as[UnknownTag2])
|
||||
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo])
|
||||
.typecase(4, dataCodec(bits).as[UnknownTag4])
|
||||
.typecase(5, dataCodec(bits).as[Features])
|
||||
.typecase(5, dataCodec(bits).as[PaymentRequestFeatures])
|
||||
.typecase(6, dataCodec(bits).as[Expiry])
|
||||
.typecase(7, dataCodec(bits).as[UnknownTag7])
|
||||
.typecase(8, dataCodec(bits).as[UnknownTag8])
|
||||
|
|
|
@ -62,10 +62,10 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu
|
|||
// Once we're confident most of the network has upgraded, we should switch to mandatory payment secrets.
|
||||
val features = {
|
||||
val f1 = Seq(Features.PaymentSecret.optional, Features.VariableLengthOnion.optional)
|
||||
val allowMultiPart = Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment)
|
||||
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment)
|
||||
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil
|
||||
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil
|
||||
Some(PaymentRequest.Features(f1 ++ f2 ++ f3: _*))
|
||||
Some(PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*))
|
||||
}
|
||||
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features)
|
||||
log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
|
||||
|
|
|
@ -29,7 +29,7 @@ import fr.acinq.eclair.payment.{IncomingPacket, PaymentFailed, PaymentSent}
|
|||
import fr.acinq.eclair.transactions.DirectedHtlc.outgoing
|
||||
import fr.acinq.eclair.transactions.OutgoingHtlc
|
||||
import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.{LongToBtcAmount, NodeParams}
|
||||
import fr.acinq.eclair.{Features, LongToBtcAmount, NodeParams}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.compat.Platform
|
||||
|
@ -294,7 +294,7 @@ object PostRestartHtlcCleaner {
|
|||
* Outgoing HTLC sets that are still pending may either succeed or fail: we need to watch them to properly forward the
|
||||
* result upstream to preserve channels.
|
||||
*/
|
||||
private def checkBrokenHtlcs(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey, features: ByteVector)(implicit log: LoggingAdapter): BrokenHtlcs = {
|
||||
private def checkBrokenHtlcs(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey, features: Features)(implicit log: LoggingAdapter): BrokenHtlcs = {
|
||||
// We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed).
|
||||
// They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when
|
||||
// we subsequently sign it. That's why we need to look in *their* commitment with direction=OUT.
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package fr.acinq.eclair.payment.send
|
||||
|
||||
import scodec.bits.BitVector
|
||||
import fr.acinq.eclair.Features
|
||||
|
||||
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: BitVector) extends InvalidInvoice
|
||||
case class UnsupportedFeatures(features: Features) extends InvalidInvoice
|
||||
/** The invoice is missing a payment secret. */
|
||||
case object PaymentSecretMissing extends InvalidInvoice
|
||||
// @formatter:on
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.UUID
|
|||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.Features.BasicMultiPartPayment
|
||||
import fr.acinq.eclair.channel.{Channel, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
|
@ -31,7 +32,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentTo
|
|||
import fr.acinq.eclair.router.Router.{Hop, NodeHop, Route, RouteParams}
|
||||
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
|
||||
|
||||
/**
|
||||
* Created by PM on 29/08/2016.
|
||||
|
@ -50,8 +51,8 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
|
|||
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
|
||||
r.paymentRequest match {
|
||||
case Some(invoice) if !invoice.features.supported =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, UnsupportedFeatures(invoice.features.bitmask)) :: Nil)
|
||||
case Some(invoice) if invoice.features.allowMultiPart && Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, UnsupportedFeatures(invoice.features.features)) :: Nil)
|
||||
case Some(invoice) if invoice.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
|
||||
invoice.paymentSecret match {
|
||||
case Some(paymentSecret) =>
|
||||
spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
|
||||
|
|
|
@ -95,7 +95,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
// If the sender already provided a route to the target, no need to involve the router.
|
||||
self ! RouteResponse(Seq(Route(c.finalPayload.amount, Nil, allowEmpty = true)))
|
||||
} else {
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, routeParams = c.routeParams, ignoreNodes = ignoredNodes)
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), c.assistedRoutes, routeParams = c.routeParams, ignoreNodes = ignoredNodes)
|
||||
}
|
||||
if (cfg.storeInDb) {
|
||||
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
|
||||
|
@ -204,12 +204,12 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
case extraHop => extraHop
|
||||
})
|
||||
// let's try again, router will have updated its state
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, assistedRoutes1, ignoreNodes, ignoreChannels, c.routeParams)
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), assistedRoutes1, ignoreNodes, ignoreChannels, c.routeParams)
|
||||
ignoreNodes
|
||||
} else {
|
||||
// this node is fishy, it gave us a bad sig!! let's filter it out
|
||||
log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}")
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams)
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams)
|
||||
ignoreNodes + nodeId
|
||||
}
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(route), e), ignoreNodes1, ignoreChannels)
|
||||
|
@ -267,7 +267,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
|
||||
private def retry(failure: PaymentFailure, data: WaitingForComplete): FSM.State[PaymentLifecycle.State, PaymentLifecycle.Data] = {
|
||||
val (ignoreNodes1, ignoreChannels1) = PaymentFailure.updateIgnored(failure, data.ignoreNodes, data.ignoreChannels)
|
||||
router ! RouteRequest(data.c.getRouteRequestStart(nodeParams), data.c.targetNodeId, data.c.finalPayload.amount, data.c.assistedRoutes, ignoreNodes1, ignoreChannels1, data.c.routeParams)
|
||||
router ! RouteRequest(data.c.getRouteRequestStart(nodeParams), data.c.targetNodeId, data.c.finalPayload.amount, data.c.getMaxFee(nodeParams), data.c.assistedRoutes, ignoreNodes1, ignoreChannels1, data.c.routeParams)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.sender, data.c, data.failures :+ failure, ignoreNodes1, ignoreChannels1)
|
||||
}
|
||||
|
||||
|
@ -340,6 +340,9 @@ object PaymentLifecycle {
|
|||
routePrefix: Seq[ChannelHop] = Nil) {
|
||||
require(finalPayload.amount > 0.msat, s"amount must be > 0")
|
||||
|
||||
def getMaxFee(nodeParams: NodeParams): MilliSatoshi =
|
||||
routeParams.getOrElse(RouteCalculation.getDefaultRouteParams(nodeParams.routerConf)).getMaxFee(finalPayload.amount)
|
||||
|
||||
/** Returns the node from which the path-finding algorithm should start. */
|
||||
def getRouteRequestStart(nodeParams: NodeParams): PublicKey = routePrefix match {
|
||||
case Nil => nodeParams.nodeId
|
||||
|
|
|
@ -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._
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, ShortChannelId, serializationResult}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, serializationResult}
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import shapeless.HNil
|
||||
|
||||
|
@ -31,16 +31,16 @@ import scala.concurrent.duration._
|
|||
*/
|
||||
object Announcements {
|
||||
|
||||
def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: ByteVector, unknownFields: ByteVector): ByteVector =
|
||||
def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features, unknownFields: ByteVector): ByteVector =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: unknownFields :: HNil))))
|
||||
|
||||
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: ByteVector, addresses: List[NodeAddress], unknownFields: ByteVector): ByteVector =
|
||||
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features, addresses: List[NodeAddress], unknownFields: ByteVector): ByteVector =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: unknownFields :: HNil))))
|
||||
|
||||
def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: Long, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], unknownFields: ByteVector): ByteVector =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: unknownFields :: HNil))))
|
||||
|
||||
def signChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = {
|
||||
def signChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64) = {
|
||||
val witness = if (isNode1(localNodeSecret.publicKey, remoteNodeId)) {
|
||||
channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeSecret.publicKey, remoteNodeId, localFundingPrivKey.publicKey, remoteFundingKey, features, unknownFields = ByteVector.empty)
|
||||
} else {
|
||||
|
@ -68,12 +68,12 @@ object Announcements {
|
|||
nodeId2 = nodeId2,
|
||||
bitcoinKey1 = bitcoinKey1,
|
||||
bitcoinKey2 = bitcoinKey2,
|
||||
features = ByteVector.empty,
|
||||
features = Features.empty,
|
||||
chainHash = chainHash
|
||||
)
|
||||
}
|
||||
|
||||
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: ByteVector, timestamp: Long = System.currentTimeMillis.milliseconds.toSeconds): NodeAnnouncement = {
|
||||
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features, timestamp: Long = System.currentTimeMillis.milliseconds.toSeconds): NodeAnnouncement = {
|
||||
require(alias.length <= 32)
|
||||
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, nodeAddresses, unknownFields = ByteVector.empty)
|
||||
val sig = Crypto.sign(witness, nodeSecret)
|
||||
|
|
|
@ -26,6 +26,7 @@ object Monitoring {
|
|||
|
||||
object Metrics {
|
||||
val FindRouteDuration = Kamon.timer("router.find-route.duration", "Path-finding duration")
|
||||
val FindRouteErrors = Kamon.counter("router.find-route.errors", "Path-finding errors")
|
||||
val RouteLength = Kamon.histogram("router.find-route.length", "Path-finding result length")
|
||||
|
||||
object QueryChannelRange {
|
||||
|
@ -69,6 +70,7 @@ object Monitoring {
|
|||
val Amount = "amount"
|
||||
val Announced = "announced"
|
||||
val Direction = "direction"
|
||||
val Error = "error"
|
||||
val NumberOfRoutes = "numRoutes"
|
||||
|
||||
object Directions {
|
||||
|
|
|
@ -29,8 +29,9 @@ import fr.acinq.eclair.router.Router._
|
|||
import fr.acinq.eclair.wire.ChannelUpdate
|
||||
import fr.acinq.eclair.{ShortChannelId, _}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Random, Try}
|
||||
import scala.util.{Failure, Random, Success, Try}
|
||||
|
||||
object RouteCalculation {
|
||||
|
||||
|
@ -71,9 +72,16 @@ object RouteCalculation {
|
|||
|
||||
log.info(s"finding a route ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignoreNodes.map(_.value).mkString(","), r.ignoreChannels.mkString(","), d.excludedChannels.mkString(","))
|
||||
log.info(s"finding a route with randomize={} params={}", routesToFind > 1, params)
|
||||
findRoute(d.graph, r.source, r.target, r.amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredEdges, ignoredVertices = r.ignoreNodes, routeParams = params, currentBlockHeight)
|
||||
.map(route => ctx.sender ! RouteResponse(route :: Nil))
|
||||
.recover { case t => ctx.sender ! Status.Failure(t) }
|
||||
KamonExt.time(Metrics.FindRouteDuration.withTag(Tags.NumberOfRoutes, routesToFind).withTag(Tags.Amount, Tags.amountBucket(r.amount))) {
|
||||
findRoute(d.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignoreNodes, params, currentBlockHeight) match {
|
||||
case Success(routes) =>
|
||||
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(r.amount)).record(routes.head.length)
|
||||
ctx.sender ! RouteResponse(routes)
|
||||
case Failure(t) =>
|
||||
Metrics.FindRouteErrors.withTag(Tags.Amount, Tags.amountBucket(r.amount)).withTag(Tags.Error, t.getClass.getSimpleName).increment()
|
||||
ctx.sender ! Status.Failure(t)
|
||||
}
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
|
@ -146,63 +154,68 @@ object RouteCalculation {
|
|||
* @param g graph of the whole network
|
||||
* @param localNodeId sender node (payer)
|
||||
* @param targetNodeId target node (final recipient)
|
||||
* @param amount the amount that will be sent along this route
|
||||
* @param numRoutes the number of shortest-paths to find
|
||||
* @param amount the amount that the target node should receive
|
||||
* @param maxFee the maximum fee of a resulting route
|
||||
* @param numRoutes the number of routes to find
|
||||
* @param extraEdges a set of extra edges we want to CONSIDER during the search
|
||||
* @param ignoredEdges a set of extra edges we want to IGNORE during the search
|
||||
* @param routeParams a set of parameters that can restrict the route search
|
||||
* @return the computed route to the destination @targetNodeId
|
||||
* @return the computed routes to the destination @param targetNodeId
|
||||
*/
|
||||
def findRoute(g: DirectedGraph,
|
||||
localNodeId: PublicKey,
|
||||
targetNodeId: PublicKey,
|
||||
amount: MilliSatoshi,
|
||||
maxFee: MilliSatoshi,
|
||||
numRoutes: Int,
|
||||
extraEdges: Set[GraphEdge] = Set.empty,
|
||||
ignoredEdges: Set[ChannelDesc] = Set.empty,
|
||||
ignoredVertices: Set[PublicKey] = Set.empty,
|
||||
routeParams: RouteParams,
|
||||
currentBlockHeight: Long): Try[Route] = Try {
|
||||
|
||||
if (localNodeId == targetNodeId) throw CannotRouteToSelf
|
||||
|
||||
def feeBaseOk(fee: MilliSatoshi): Boolean = fee <= routeParams.maxFeeBase
|
||||
|
||||
def feePctOk(fee: MilliSatoshi, amount: MilliSatoshi): Boolean = {
|
||||
val maxFee = amount * routeParams.maxFeePct
|
||||
fee <= maxFee
|
||||
currentBlockHeight: Long): Try[Seq[Route]] = Try {
|
||||
findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight) match {
|
||||
case Right(routes) => routes.map(route => Route(amount, route.path.map(graphEdgeToHop)))
|
||||
case Left(ex) => return Failure(ex)
|
||||
}
|
||||
}
|
||||
|
||||
def feeOk(fee: MilliSatoshi, amount: MilliSatoshi): Boolean = feeBaseOk(fee) || feePctOk(fee, amount)
|
||||
@tailrec
|
||||
private def findRouteInternal(g: DirectedGraph,
|
||||
localNodeId: PublicKey,
|
||||
targetNodeId: PublicKey,
|
||||
amount: MilliSatoshi,
|
||||
maxFee: MilliSatoshi,
|
||||
numRoutes: Int,
|
||||
extraEdges: Set[GraphEdge] = Set.empty,
|
||||
ignoredEdges: Set[ChannelDesc] = Set.empty,
|
||||
ignoredVertices: Set[PublicKey] = Set.empty,
|
||||
routeParams: RouteParams,
|
||||
currentBlockHeight: Long): Either[RouterException, Seq[Graph.WeightedPath]] = {
|
||||
if (localNodeId == targetNodeId) return Left(CannotRouteToSelf)
|
||||
|
||||
def feeOk(fee: MilliSatoshi): Boolean = fee <= maxFee
|
||||
|
||||
def lengthOk(length: Int): Boolean = length <= routeParams.routeMaxLength && length <= ROUTE_MAX_LENGTH
|
||||
|
||||
def cltvOk(cltv: CltvExpiryDelta): Boolean = cltv <= routeParams.routeMaxCltv
|
||||
|
||||
val boundaries: RichWeight => Boolean = { weight =>
|
||||
feeOk(weight.cost - amount, amount) && lengthOk(weight.length) && cltvOk(weight.cltv)
|
||||
}
|
||||
val boundaries: RichWeight => Boolean = { weight => feeOk(weight.cost - amount) && lengthOk(weight.length) && cltvOk(weight.cltv) }
|
||||
|
||||
val foundRoutes = KamonExt.time(Metrics.FindRouteDuration.withTag(Tags.NumberOfRoutes, numRoutes).withTag(Tags.Amount, Tags.amountBucket(amount))) {
|
||||
Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList
|
||||
}
|
||||
foundRoutes match {
|
||||
case Nil if routeParams.routeMaxLength < ROUTE_MAX_LENGTH => // if not found within the constraints we relax and repeat the search
|
||||
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(amount)).record(0)
|
||||
return findRoute(g, localNodeId, targetNodeId, amount, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV), currentBlockHeight)
|
||||
case Nil =>
|
||||
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(amount)).record(0)
|
||||
throw RouteNotFound
|
||||
case foundRoutes =>
|
||||
val routes = foundRoutes.find(_.path.size == 1) match {
|
||||
case Some(directRoute) => directRoute :: Nil
|
||||
case _ => foundRoutes
|
||||
}
|
||||
// At this point 'routes' cannot be empty
|
||||
val randomizedRoutes = if (routeParams.randomize) Random.shuffle(routes) else routes
|
||||
val route = randomizedRoutes.head.path.map(graphEdgeToHop)
|
||||
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(amount)).record(route.length)
|
||||
Route(amount, route)
|
||||
val foundRoutes: Seq[Graph.WeightedPath] = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries)
|
||||
if (foundRoutes.nonEmpty) {
|
||||
val (directRoutes, indirectRoutes) = foundRoutes.partition(_.path.length == 1)
|
||||
val routes = if (routeParams.randomize) {
|
||||
Random.shuffle(directRoutes) ++ Random.shuffle(indirectRoutes)
|
||||
} else {
|
||||
directRoutes ++ indirectRoutes
|
||||
}
|
||||
Right(routes)
|
||||
} else if (routeParams.routeMaxLength < ROUTE_MAX_LENGTH) {
|
||||
// if not found within the constraints we relax and repeat the search
|
||||
val relaxedRouteParams = routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV)
|
||||
findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, relaxedRouteParams, currentBlockHeight)
|
||||
} else {
|
||||
Left(RouteNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -330,11 +330,17 @@ object Router {
|
|||
override def fee(amount: MilliSatoshi): MilliSatoshi = fee
|
||||
}
|
||||
|
||||
case class RouteParams(randomize: Boolean, maxFeeBase: MilliSatoshi, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: CltvExpiryDelta, ratios: Option[WeightRatios])
|
||||
case class RouteParams(randomize: Boolean, maxFeeBase: MilliSatoshi, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: CltvExpiryDelta, ratios: Option[WeightRatios]) {
|
||||
def getMaxFee(amount: MilliSatoshi): MilliSatoshi = {
|
||||
// The payment fee must satisfy either the flat fee or the percentage fee, not necessarily both.
|
||||
maxFeeBase.max(amount * maxFeePct)
|
||||
}
|
||||
}
|
||||
|
||||
case class RouteRequest(source: PublicKey,
|
||||
target: PublicKey,
|
||||
amount: MilliSatoshi,
|
||||
maxFee: MilliSatoshi,
|
||||
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
||||
ignoreNodes: Set[PublicKey] = Set.empty,
|
||||
ignoreChannels: Set[ChannelDesc] = Set.empty,
|
||||
|
|
|
@ -18,7 +18,7 @@ package fr.acinq.eclair.wire
|
|||
|
||||
import fr.acinq.eclair.wire.CommonCodecs._
|
||||
import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags}
|
||||
import fr.acinq.eclair.{KamonExt, wire}
|
||||
import fr.acinq.eclair.{Features, KamonExt, wire}
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.codecs._
|
||||
import scodec.{Attempt, Codec}
|
||||
|
@ -28,15 +28,20 @@ import scodec.{Attempt, Codec}
|
|||
*/
|
||||
object LightningMessageCodecs {
|
||||
|
||||
val featuresCodec: Codec[Features] = varsizebinarydata.xmap[Features](
|
||||
{ bytes => Features(bytes) },
|
||||
{ features => features.toByteVector }
|
||||
)
|
||||
|
||||
/** 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[ByteVector] = (
|
||||
val combinedFeaturesCodec: Codec[Features] = (
|
||||
("globalFeatures" | varsizebinarydata) ::
|
||||
("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[ByteVector](
|
||||
("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features](
|
||||
{ case (gf, lf) =>
|
||||
val length = gf.length.max(lf.length)
|
||||
gf.padLeft(length) | lf.padLeft(length)
|
||||
Features(gf.padLeft(length) | lf.padLeft(length))
|
||||
},
|
||||
{ features => (ByteVector.empty, features) })
|
||||
{ features => (ByteVector.empty, features.toByteVector) })
|
||||
|
||||
val initCodec: Codec[Init] = (("features" | combinedFeaturesCodec) :: ("tlvStream" | InitTlvCodecs.initTlvCodec)).as[Init]
|
||||
|
||||
|
@ -165,7 +170,7 @@ object LightningMessageCodecs {
|
|||
("bitcoinSignature" | bytes64)).as[AnnouncementSignatures]
|
||||
|
||||
val channelAnnouncementWitnessCodec =
|
||||
("features" | varsizebinarydata) ::
|
||||
("features" | featuresCodec) ::
|
||||
("chainHash" | bytes32) ::
|
||||
("shortChannelId" | shortchannelid) ::
|
||||
("nodeId1" | publicKey) ::
|
||||
|
@ -182,7 +187,7 @@ object LightningMessageCodecs {
|
|||
channelAnnouncementWitnessCodec).as[ChannelAnnouncement]
|
||||
|
||||
val nodeAnnouncementWitnessCodec =
|
||||
("features" | varsizebinarydata) ::
|
||||
("features" | featuresCodec) ::
|
||||
("timestamp" | uint32) ::
|
||||
("nodeId" | publicKey) ::
|
||||
("rgbColor" | rgb) ::
|
||||
|
|
|
@ -23,7 +23,7 @@ import com.google.common.base.Charsets
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi}
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64}
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.Try
|
||||
|
@ -46,7 +46,7 @@ sealed trait HasChainHash extends LightningMessage { def chainHash: ByteVector32
|
|||
sealed trait UpdateMessage extends HtlcMessage // <- not in the spec
|
||||
// @formatter:on
|
||||
|
||||
case class Init(features: ByteVector, tlvs: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage {
|
||||
case class Init(features: Features, tlvs: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage {
|
||||
val networks = tlvs.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil)
|
||||
}
|
||||
|
||||
|
@ -162,7 +162,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64,
|
|||
nodeSignature2: ByteVector64,
|
||||
bitcoinSignature1: ByteVector64,
|
||||
bitcoinSignature2: ByteVector64,
|
||||
features: ByteVector,
|
||||
features: Features,
|
||||
chainHash: ByteVector32,
|
||||
shortChannelId: ShortChannelId,
|
||||
nodeId1: PublicKey,
|
||||
|
@ -206,7 +206,7 @@ case class Tor3(tor3: String, port: Int) extends OnionAddress { override def soc
|
|||
|
||||
|
||||
case class NodeAnnouncement(signature: ByteVector64,
|
||||
features: ByteVector,
|
||||
features: Features,
|
||||
timestamp: Long,
|
||||
nodeId: PublicKey,
|
||||
rgbColor: Color,
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package fr.acinq.eclair
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||
import fr.acinq.eclair.Features._
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits._
|
||||
|
@ -27,28 +29,29 @@ import scodec.bits._
|
|||
class FeaturesSpec extends AnyFunSuite {
|
||||
|
||||
test("'initial_routing_sync' feature") {
|
||||
assert(hasFeature(hex"08", InitialRoutingSync, Some(FeatureSupport.Optional)))
|
||||
assert(!hasFeature(hex"08", InitialRoutingSync, Some(FeatureSupport.Mandatory)))
|
||||
assert(Features(hex"08").hasFeature(InitialRoutingSync, Some(FeatureSupport.Optional)))
|
||||
assert(!Features(hex"08").hasFeature(InitialRoutingSync, Some(FeatureSupport.Mandatory)))
|
||||
}
|
||||
|
||||
test("'data_loss_protect' feature") {
|
||||
assert(hasFeature(hex"01", OptionDataLossProtect, Some(FeatureSupport.Mandatory)))
|
||||
assert(hasFeature(hex"02", OptionDataLossProtect, Some(FeatureSupport.Optional)))
|
||||
assert(Features(hex"01").hasFeature(OptionDataLossProtect, Some(FeatureSupport.Mandatory)))
|
||||
assert(Features(hex"02").hasFeature(OptionDataLossProtect, Some(FeatureSupport.Optional)))
|
||||
}
|
||||
|
||||
test("'initial_routing_sync', 'data_loss_protect' and 'variable_length_onion' features") {
|
||||
val features = hex"010a"
|
||||
val features = Features(Set(ActivatedFeature(InitialRoutingSync, Optional), ActivatedFeature(OptionDataLossProtect, Optional), ActivatedFeature(VariableLengthOnion, Mandatory)))
|
||||
assert(features.toByteVector == hex"010a")
|
||||
assert(areSupported(features))
|
||||
assert(hasFeature(features, OptionDataLossProtect))
|
||||
assert(hasFeature(features, InitialRoutingSync, None))
|
||||
assert(hasFeature(features, VariableLengthOnion))
|
||||
assert(features.hasFeature(OptionDataLossProtect))
|
||||
assert(features.hasFeature(InitialRoutingSync, None))
|
||||
assert(features.hasFeature(VariableLengthOnion))
|
||||
}
|
||||
|
||||
test("'variable_length_onion' feature") {
|
||||
assert(hasFeature(hex"0100", VariableLengthOnion))
|
||||
assert(hasFeature(hex"0100", VariableLengthOnion, Some(FeatureSupport.Mandatory)))
|
||||
assert(hasFeature(hex"0200", VariableLengthOnion, None))
|
||||
assert(hasFeature(hex"0200", VariableLengthOnion, Some(FeatureSupport.Optional)))
|
||||
assert(Features(hex"0100").hasFeature(VariableLengthOnion))
|
||||
assert(Features(hex"0100").hasFeature(VariableLengthOnion, Some(FeatureSupport.Mandatory)))
|
||||
assert(Features(hex"0200").hasFeature(VariableLengthOnion, None))
|
||||
assert(Features(hex"0200").hasFeature(VariableLengthOnion, Some(FeatureSupport.Optional)))
|
||||
}
|
||||
|
||||
test("features dependencies") {
|
||||
|
@ -76,31 +79,31 @@ class FeaturesSpec extends AnyFunSuite {
|
|||
|
||||
for ((testCase, valid) <- testCases) {
|
||||
if (valid) {
|
||||
assert(validateFeatureGraph(testCase) === None)
|
||||
assert(validateFeatureGraph(testCase.bytes) === None)
|
||||
assert(validateFeatureGraph(Features(testCase)) === None)
|
||||
assert(validateFeatureGraph(Features(testCase.bytes)) === None)
|
||||
} else {
|
||||
assert(validateFeatureGraph(testCase).nonEmpty)
|
||||
assert(validateFeatureGraph(testCase.bytes).nonEmpty)
|
||||
assert(validateFeatureGraph(Features(testCase)).nonEmpty)
|
||||
assert(validateFeatureGraph(Features(testCase.bytes)).nonEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("features compatibility") {
|
||||
assert(areSupported(ByteVector.fromLong(1L << InitialRoutingSync.optional)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << OptionDataLossProtect.mandatory)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << OptionDataLossProtect.optional)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueries.mandatory)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueries.optional)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << VariableLengthOnion.mandatory)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << VariableLengthOnion.optional)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueriesExtended.mandatory)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueriesExtended.optional)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << PaymentSecret.mandatory)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << PaymentSecret.optional)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << BasicMultiPartPayment.mandatory)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << BasicMultiPartPayment.optional)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << Wumbo.mandatory)))
|
||||
assert(areSupported(ByteVector.fromLong(1L << Wumbo.optional)))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(InitialRoutingSync, Optional)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(OptionDataLossProtect, Mandatory)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(OptionDataLossProtect, Optional)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueries, Mandatory)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueriesExtended, Mandatory)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueriesExtended, Optional)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(VariableLengthOnion, Mandatory)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(VariableLengthOnion, Optional)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(PaymentSecret, Mandatory)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(PaymentSecret, Optional)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(BasicMultiPartPayment, Mandatory)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(BasicMultiPartPayment, Optional)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(Wumbo, Mandatory)))))
|
||||
assert(areSupported(Features(Set(ActivatedFeature(Wumbo, Optional)))))
|
||||
|
||||
val testCases = Map(
|
||||
bin" 00000000000000001011" -> true,
|
||||
|
@ -124,8 +127,128 @@ class FeaturesSpec extends AnyFunSuite {
|
|||
bin"01000000000000000000000000000000" -> false
|
||||
)
|
||||
for ((testCase, expected) <- testCases) {
|
||||
assert(areSupported(testCase) === expected, testCase)
|
||||
assert(areSupported(Features(testCase)) === expected, testCase.toBin)
|
||||
}
|
||||
}
|
||||
|
||||
test("features to bytes") {
|
||||
val testCases = Map(
|
||||
hex"" -> Features.empty,
|
||||
hex"0100" -> Features(Set(ActivatedFeature(VariableLengthOnion, Mandatory))),
|
||||
hex"028a8a" -> Features(Set(ActivatedFeature(OptionDataLossProtect, Optional), ActivatedFeature(InitialRoutingSync, Optional), ActivatedFeature(ChannelRangeQueries, Optional), ActivatedFeature(VariableLengthOnion, Optional), ActivatedFeature(ChannelRangeQueriesExtended, Optional), ActivatedFeature(PaymentSecret, Optional), ActivatedFeature(BasicMultiPartPayment, Optional))),
|
||||
hex"09004200" -> Features(Set(ActivatedFeature(VariableLengthOnion, Optional), ActivatedFeature(PaymentSecret, Mandatory)), Set(UnknownFeature(24), UnknownFeature(27))),
|
||||
hex"52000000" -> Features(Set.empty, Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30)))
|
||||
)
|
||||
|
||||
for ((bin, features) <- testCases) {
|
||||
assert(features.toByteVector === bin)
|
||||
assert(Features(bin) === features)
|
||||
val notMinimallyEncoded = Features(hex"00" ++ bin)
|
||||
assert(notMinimallyEncoded === features)
|
||||
assert(notMinimallyEncoded.toByteVector === bin) // features are minimally-encoded when converting to bytes
|
||||
}
|
||||
}
|
||||
|
||||
test("parse features from configuration") {
|
||||
{
|
||||
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)
|
||||
|
||||
val features = fromConfiguration(conf)
|
||||
assert(features.toByteVector === hex"028a8a")
|
||||
assert(Features(hex"028a8a") === features)
|
||||
assert(areSupported(features))
|
||||
assert(validateFeatureGraph(features) === None)
|
||||
assert(features.hasFeature(OptionDataLossProtect, Some(Optional)))
|
||||
assert(features.hasFeature(InitialRoutingSync, Some(Optional)))
|
||||
assert(features.hasFeature(ChannelRangeQueries, Some(Optional)))
|
||||
assert(features.hasFeature(ChannelRangeQueriesExtended, Some(Optional)))
|
||||
assert(features.hasFeature(VariableLengthOnion, Some(Optional)))
|
||||
assert(features.hasFeature(PaymentSecret, Some(Optional)))
|
||||
assert(features.hasFeature(BasicMultiPartPayment, Some(Optional)))
|
||||
}
|
||||
|
||||
{
|
||||
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
|
||||
)
|
||||
|
||||
val features = fromConfiguration(conf)
|
||||
assert(features.toByteVector === hex"068a")
|
||||
assert(Features(hex"068a") === features)
|
||||
assert(areSupported(features))
|
||||
assert(validateFeatureGraph(features) === None)
|
||||
assert(features.hasFeature(OptionDataLossProtect, Some(Optional)))
|
||||
assert(features.hasFeature(InitialRoutingSync, Some(Optional)))
|
||||
assert(!features.hasFeature(InitialRoutingSync, Some(Mandatory)))
|
||||
assert(features.hasFeature(ChannelRangeQueries, Some(Optional)))
|
||||
assert(features.hasFeature(ChannelRangeQueriesExtended, Some(Mandatory)))
|
||||
assert(features.hasFeature(VariableLengthOnion, Some(Optional)))
|
||||
assert(!features.hasFeature(PaymentSecret))
|
||||
}
|
||||
|
||||
{
|
||||
val confWithUnknownFeatures = ConfigFactory.parseString(
|
||||
"""
|
||||
|features {
|
||||
| option_non_existent = mandatory # this is ignored
|
||||
| gossip_queries = optional
|
||||
| payment_secret = mandatory
|
||||
|}
|
||||
""".stripMargin)
|
||||
|
||||
val features = fromConfiguration(confWithUnknownFeatures)
|
||||
assert(features.toByteVector === hex"4080")
|
||||
assert(Features(hex"4080") === features)
|
||||
assert(areSupported(features))
|
||||
assert(features.hasFeature(ChannelRangeQueries, Some(Optional)))
|
||||
assert(features.hasFeature(PaymentSecret, Some(Mandatory)))
|
||||
}
|
||||
|
||||
{
|
||||
val confWithUnknownSupport = ConfigFactory.parseString(
|
||||
"""
|
||||
|features {
|
||||
| option_data_loss_protect = what
|
||||
| gossip_queries = optional
|
||||
| payment_secret = mandatory
|
||||
|}
|
||||
""".stripMargin)
|
||||
|
||||
assertThrows[RuntimeException](fromConfiguration(confWithUnknownSupport))
|
||||
}
|
||||
}
|
||||
|
||||
test("'knownFeatures' contains all our known features (reflection test)") {
|
||||
import scala.reflect.runtime.universe._
|
||||
import scala.reflect.runtime.{ universe => runtime }
|
||||
val mirror = runtime.runtimeMirror(ClassLoader.getSystemClassLoader)
|
||||
val subclasses = typeOf[Feature].typeSymbol.asClass.knownDirectSubclasses
|
||||
val knownFeatures = subclasses.map({ desc =>
|
||||
val mod = mirror.staticModule(desc.asClass.fullName)
|
||||
mirror.reflectModule(mod).instance.asInstanceOf[Feature]
|
||||
})
|
||||
|
||||
assert((knownFeatures -- Features.knownFeatures).isEmpty)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ class JsonSerializersSpec extends AnyFunSuite with Logging {
|
|||
maxAcceptedHtlcs = Random.nextInt(Short.MaxValue),
|
||||
defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)),
|
||||
isFunder = Random.nextBoolean(),
|
||||
features = randomBytes(256))
|
||||
features = Features(randomBytes(256)))
|
||||
|
||||
logger.info(write(localParams))
|
||||
|
||||
|
@ -83,7 +83,7 @@ class JsonSerializersSpec extends AnyFunSuite with Logging {
|
|||
paymentBasepoint = randomKey.publicKey,
|
||||
delayedPaymentBasepoint = randomKey.publicKey,
|
||||
htlcBasepoint = randomKey.publicKey,
|
||||
features = randomBytes(256))
|
||||
features = Features(randomBytes(256)))
|
||||
|
||||
logger.info(write(remoteParams))
|
||||
}
|
||||
|
|
|
@ -20,7 +20,11 @@ import java.util.concurrent.atomic.AtomicLong
|
|||
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Block
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.FeatureSupport.Mandatory
|
||||
import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, PaymentSecret, VariableLengthOnion}
|
||||
import fr.acinq.eclair.crypto.LocalKeyManager
|
||||
import scodec.bits.ByteVector
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
@ -64,7 +68,7 @@ class StartupSpec extends AnyFunSuite {
|
|||
assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long"))
|
||||
}
|
||||
|
||||
test("NodeParams should fail with deprecated global-features or local-features") {
|
||||
test("NodeParams should fail with deprecated global-features, local-features or hex features") {
|
||||
for (deprecated <- Seq("global-features", "local-features")) {
|
||||
val illegalGlobalFeaturesConf = ConfigFactory.parseString(deprecated + " = \"0200\"")
|
||||
val conf = illegalGlobalFeaturesConf.withFallback(defaultConf)
|
||||
|
@ -72,17 +76,59 @@ class StartupSpec extends AnyFunSuite {
|
|||
val nodeParamsAttempt = Try(makeNodeParamsWithDefaults(conf))
|
||||
assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains(deprecated))
|
||||
}
|
||||
|
||||
val illegalByteVectorFeatures = ConfigFactory.parseString("features = \"0200\"")
|
||||
val conf = illegalByteVectorFeatures.withFallback(defaultConf)
|
||||
val nodeParamsAttempt = Try(makeNodeParamsWithDefaults(conf))
|
||||
assert(nodeParamsAttempt.failed.get.getMessage == "requirement failed: configuration key 'features' have moved from bytevector to human readable (ex: 'feature-name' = optional/mandatory)")
|
||||
}
|
||||
|
||||
test("NodeParams should fail if features are inconsistent") {
|
||||
val legalFeaturesConf = ConfigFactory.parseString("features = \"028a8a\"")
|
||||
val illegalButAllowedFeaturesConf = ConfigFactory.parseString("features = \"028000\"") // basic_mpp without var_onion_optin
|
||||
val illegalFeaturesConf = ConfigFactory.parseString("features = \"020000\"") // basic_mpp without payment_secret
|
||||
val legalFeaturesConf = ConfigFactory.parseMap(Map(
|
||||
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
|
||||
s"features.${InitialRoutingSync.rfcName}" -> "optional",
|
||||
s"features.${ChannelRangeQueries.rfcName}" -> "optional",
|
||||
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional",
|
||||
s"features.${VariableLengthOnion.rfcName}" -> "optional",
|
||||
s"features.${PaymentSecret.rfcName}" -> "optional",
|
||||
s"features.${BasicMultiPartPayment.rfcName}" -> "optional"
|
||||
).asJava)
|
||||
|
||||
// basic_mpp without var_onion_optin
|
||||
val illegalButAllowedFeaturesConf = ConfigFactory.parseMap(Map(
|
||||
s"features.${PaymentSecret.rfcName}" -> "optional",
|
||||
s"features.${BasicMultiPartPayment.rfcName}" -> "optional"
|
||||
).asJava)
|
||||
|
||||
// basic_mpp without payment_secret
|
||||
val illegalFeaturesConf = ConfigFactory.parseMap(Map(
|
||||
s"features.${BasicMultiPartPayment.rfcName}" -> "optional"
|
||||
).asJava)
|
||||
|
||||
assert(Try(makeNodeParamsWithDefaults(legalFeaturesConf.withFallback(defaultConf))).isSuccess)
|
||||
assert(Try(makeNodeParamsWithDefaults(illegalButAllowedFeaturesConf.withFallback(defaultConf))).isSuccess)
|
||||
assert(Try(makeNodeParamsWithDefaults(illegalFeaturesConf.withFallback(defaultConf))).isFailure)
|
||||
}
|
||||
|
||||
test("parse human readable override features") {
|
||||
val perNodeConf = ConfigFactory.parseString(
|
||||
"""
|
||||
| override-features = [ // optional per-node features
|
||||
| {
|
||||
| nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
| features {
|
||||
| basic_mpp = mandatory
|
||||
| }
|
||||
| }
|
||||
| ]
|
||||
""".stripMargin
|
||||
)
|
||||
|
||||
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
|
||||
val perNodeFeatures = nodeParams.overrideFeatures(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
|
||||
assert(perNodeFeatures.hasFeature(BasicMultiPartPayment, Some(Mandatory)))
|
||||
}
|
||||
|
||||
test("NodeParams should fail if htlc-minimum-msat is set to 0") {
|
||||
val noHtlcMinimumConf = ConfigFactory.parseString("htlc-minimum-msat = 0")
|
||||
assert(Try(makeNodeParamsWithDefaults(noHtlcMinimumConf.withFallback(defaultConf))).isFailure)
|
||||
|
|
|
@ -21,6 +21,8 @@ import java.util.concurrent.atomic.AtomicLong
|
|||
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, Script}
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features.{ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, VariableLengthOnion}
|
||||
import fr.acinq.eclair.NodeParams.BITCOIND
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf}
|
||||
import fr.acinq.eclair.crypto.LocalKeyManager
|
||||
|
@ -70,7 +72,12 @@ object TestConstants {
|
|||
alias = "alice",
|
||||
color = Color(1, 2, 3),
|
||||
publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil,
|
||||
features = ByteVector.fromValidHex("0a8a"),
|
||||
features = Features(Set(
|
||||
ActivatedFeature(InitialRoutingSync, Optional),
|
||||
ActivatedFeature(OptionDataLossProtect, Optional),
|
||||
ActivatedFeature(ChannelRangeQueries, Optional),
|
||||
ActivatedFeature(ChannelRangeQueriesExtended, Optional),
|
||||
ActivatedFeature(VariableLengthOnion, Optional))),
|
||||
overrideFeatures = Map.empty,
|
||||
syncWhitelist = Set.empty,
|
||||
dustLimit = 1100 sat,
|
||||
|
@ -152,7 +159,7 @@ object TestConstants {
|
|||
alias = "bob",
|
||||
color = Color(4, 5, 6),
|
||||
publicAddresses = NodeAddress.fromParts("localhost", 9732).get :: Nil,
|
||||
features = ByteVector.fromValidHex("0200"), // variable_length_onion, no announcement
|
||||
features = Features(Set(ActivatedFeature(VariableLengthOnion, Optional))),
|
||||
overrideFeatures = Map.empty,
|
||||
syncWhitelist = Set.empty,
|
||||
dustLimit = 1000 sat,
|
||||
|
|
|
@ -490,8 +490,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
object CommitmentsSpec {
|
||||
|
||||
def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: Long = 0, dustLimit: Satoshi = 0 sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments = {
|
||||
val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, ByteVector.empty)
|
||||
val remoteParams = RemoteParams(randomKey.publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty)
|
||||
val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, Features.empty)
|
||||
val remoteParams = RemoteParams(randomKey.publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, Features.empty)
|
||||
val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, (toLocal + toRemote).truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey)
|
||||
Commitments(
|
||||
ChannelVersion.STANDARD,
|
||||
|
@ -512,8 +512,8 @@ object CommitmentsSpec {
|
|||
}
|
||||
|
||||
def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments = {
|
||||
val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, ByteVector.empty)
|
||||
val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty)
|
||||
val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, Features.empty)
|
||||
val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, Features.empty)
|
||||
val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, (toLocal + toRemote).truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey)
|
||||
Commitments(
|
||||
ChannelVersion.STANDARD,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package fr.acinq.eclair.channel
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
|
||||
import fr.acinq.eclair.Features
|
||||
import fr.acinq.eclair.channel.Commitments.msg2String
|
||||
import fr.acinq.eclair.wire.{Init, LightningMessage}
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -71,7 +72,7 @@ class FuzzyPipe(fuzzy: Boolean) extends Actor with Stash with ActorLogging {
|
|||
log.debug(f" X-${msg2String(msg)}%-6s--- B")
|
||||
case 'reconnect =>
|
||||
log.debug("RECONNECTED")
|
||||
val dummyInit = Init(ByteVector.empty)
|
||||
val dummyInit = Init(Features.empty)
|
||||
a ! INPUT_RECONNECTED(self, dummyInit, dummyInit)
|
||||
b ! INPUT_RECONNECTED(self, dummyInit, dummyInit)
|
||||
context become connected(a, b, Random.nextInt(40))
|
||||
|
|
|
@ -18,13 +18,15 @@ package fr.acinq.eclair.channel.states.a
|
|||
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.{Block, Btc, ByteVector32, Satoshi}
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features.Wumbo
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair.blockchain.{MakeFundingTxResponse, TestWallet}
|
||||
import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_INTERNAL, _}
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants, TestKitBaseClass}
|
||||
import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, TestConstants, TestKitBaseClass}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.{ByteVector, HexStringSyntax}
|
||||
|
@ -48,12 +50,12 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
import com.softwaremill.quicklens._
|
||||
val aliceNodeParams = TestConstants.Alice.nodeParams
|
||||
.modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash)
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000")
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional))))
|
||||
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100))
|
||||
|
||||
val bobNodeParams = TestConstants.Bob.nodeParams
|
||||
.modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash)
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000")
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional))))
|
||||
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100))
|
||||
|
||||
val setup = init(aliceNodeParams, bobNodeParams, wallet = noopWallet)
|
||||
|
|
|
@ -18,11 +18,13 @@ package fr.acinq.eclair.channel.states.a
|
|||
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.{Block, Btc, ByteVector32}
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features.Wumbo
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion}
|
||||
import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.{ByteVector, HexStringSyntax}
|
||||
|
@ -40,7 +42,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
|
|||
override def withFixture(test: OneArgTest): Outcome = {
|
||||
import com.softwaremill.quicklens._
|
||||
val bobNodeParams = Bob.nodeParams
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000")
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional))))
|
||||
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1))
|
||||
|
||||
val setup = init(nodeParamsB = bobNodeParams)
|
||||
|
|
|
@ -20,12 +20,14 @@ import java.sql.Connection
|
|||
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Satoshi}
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features.VariableLengthOnion
|
||||
import fr.acinq.eclair.db.sqlite.SqliteNetworkDb
|
||||
import fr.acinq.eclair.db.sqlite.SqliteUtils._
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.router.Router.PublicChannel
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, Color, NodeAddress, Tor2}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomKey}
|
||||
import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomKey}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
||||
|
@ -82,10 +84,10 @@ class SqliteNetworkDbSpec extends AnyFunSuite {
|
|||
val sqlite = TestConstants.sqliteInMemory()
|
||||
val db = new SqliteNetworkDb(sqlite, Block.RegtestGenesisBlock.hash)
|
||||
|
||||
val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"")
|
||||
val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"0200")
|
||||
val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"0200")
|
||||
val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, hex"00")
|
||||
val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty)
|
||||
val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(Set(ActivatedFeature(VariableLengthOnion, Optional))))
|
||||
val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(Set(ActivatedFeature(VariableLengthOnion, Optional))))
|
||||
val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, Features.empty)
|
||||
|
||||
assert(db.listNodes().toSet === Set.empty)
|
||||
db.addNode(node_1)
|
||||
|
|
|
@ -22,6 +22,8 @@ import akka.actor.PoisonPill
|
|||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.Block
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features.{ChannelRangeQueries, VariableLengthOnion}
|
||||
import fr.acinq.eclair.TestConstants._
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
|
@ -107,26 +109,31 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
|
|||
test("disconnect if authentication timeout") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
val origin = TestProbe()
|
||||
probe.watch(peerConnection)
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref)))
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref)))
|
||||
probe.expectTerminated(peerConnection, nodeParams.authTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here
|
||||
origin.expectMsg(PeerConnection.ConnectionResult.AuthenticationFailed("authentication timed out"))
|
||||
}
|
||||
|
||||
test("disconnect if init timeout") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
val origin = TestProbe()
|
||||
probe.watch(peerConnection)
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref)))
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref)))
|
||||
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
|
||||
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref))
|
||||
probe.expectTerminated(peerConnection, nodeParams.initTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here
|
||||
origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("initialization timed out"))
|
||||
}
|
||||
|
||||
test("disconnect if incompatible local features") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
val origin = TestProbe()
|
||||
probe.watch(transport.ref)
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref)))
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref)))
|
||||
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
|
||||
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref))
|
||||
transport.expectMsgType[TransportHandler.Listener]
|
||||
|
@ -134,13 +141,15 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
|
|||
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value)
|
||||
transport.expectMsgType[TransportHandler.ReadAck]
|
||||
probe.expectTerminated(transport.ref)
|
||||
origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features"))
|
||||
}
|
||||
|
||||
test("disconnect if incompatible global features") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
val origin = TestProbe()
|
||||
probe.watch(transport.ref)
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref)))
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref)))
|
||||
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
|
||||
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref))
|
||||
transport.expectMsgType[TransportHandler.Listener]
|
||||
|
@ -148,6 +157,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
|
|||
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value)
|
||||
transport.expectMsgType[TransportHandler.ReadAck]
|
||||
probe.expectTerminated(transport.ref)
|
||||
origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features"))
|
||||
}
|
||||
|
||||
test("masks off MPP and PaymentSecret features") { f =>
|
||||
|
@ -164,22 +174,23 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
|
|||
)
|
||||
|
||||
for ((configuredFeatures, sentFeatures) <- testCases) {
|
||||
val nodeParams = TestConstants.Alice.nodeParams.copy(features = configuredFeatures.bytes)
|
||||
val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(configuredFeatures))
|
||||
val peerConnection = TestFSMRef(new PeerConnection(nodeParams, switchboard.ref, router.ref))
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref)))
|
||||
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
|
||||
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref))
|
||||
transport.expectMsgType[TransportHandler.Listener]
|
||||
val init = transport.expectMsgType[wire.Init]
|
||||
assert(init.features === sentFeatures.bytes)
|
||||
assert(init.features.toByteVector === sentFeatures.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
test("disconnect if incompatible networks") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
val origin = TestProbe()
|
||||
probe.watch(transport.ref)
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref)))
|
||||
probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref)))
|
||||
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
|
||||
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref))
|
||||
transport.expectMsgType[TransportHandler.Listener]
|
||||
|
@ -187,23 +198,24 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
|
|||
transport.send(peerConnection, wire.Init(Bob.nodeParams.features, 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"))
|
||||
}
|
||||
|
||||
test("sync if no whitelist is defined") { f =>
|
||||
import f._
|
||||
val remoteInit = wire.Init(bin"10000000".bytes) // bob supports channel range queries
|
||||
val remoteInit = wire.Init(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional))))
|
||||
connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit, expectSync = true)
|
||||
}
|
||||
|
||||
test("sync if whitelist contains peer", Tag("sync-whitelist-bob")) { f =>
|
||||
import f._
|
||||
val remoteInit = wire.Init(bin"0000001010000000".bytes) // bob supports channel range queries and variable length onion
|
||||
val remoteInit = wire.Init(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional), ActivatedFeature(VariableLengthOnion, Optional))))
|
||||
connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit, expectSync = true)
|
||||
}
|
||||
|
||||
test("don't sync if whitelist doesn't contain peer", Tag("sync-whitelist-random")) { f =>
|
||||
import f._
|
||||
val remoteInit = wire.Init(bin"0000001010000000".bytes) // bob supports channel range queries
|
||||
val remoteInit = wire.Init(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional)))) // bob supports channel range queries
|
||||
connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit, expectSync = false)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ import akka.testkit.{TestFSMRef, TestProbe}
|
|||
import com.google.common.net.HostAndPort
|
||||
import fr.acinq.bitcoin.Btc
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features.Wumbo
|
||||
import fr.acinq.eclair.TestConstants._
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet}
|
||||
|
@ -54,12 +56,12 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe
|
|||
|
||||
import com.softwaremill.quicklens._
|
||||
val aliceParams = TestConstants.Alice.nodeParams
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000")
|
||||
.modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional))))
|
||||
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9))
|
||||
.modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true)
|
||||
|
||||
if (test.tags.contains("with_node_announcement")) {
|
||||
val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil)
|
||||
val bobAnnouncement = NodeAnnouncement(randomBytes64, Features.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil)
|
||||
aliceParams.db.network.addNode(bobAnnouncement)
|
||||
}
|
||||
|
||||
|
@ -92,7 +94,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe
|
|||
val probe = TestProbe()
|
||||
probe.send(peer, Peer.Init(Set.empty))
|
||||
probe.send(peer, Peer.Connect(remoteNodeId, address_opt = None))
|
||||
probe.expectMsg(s"no address found")
|
||||
probe.expectMsg(PeerConnection.ConnectionResult.NoAddressFound)
|
||||
}
|
||||
|
||||
test("successfully connect to peer at user request") { f =>
|
||||
|
@ -132,7 +134,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe
|
|||
val mockAddress = NodeAddress.fromParts(mockServer.getInetAddress.getHostAddress, mockServer.getLocalPort).get
|
||||
|
||||
// we put the server address in the node db
|
||||
val ann = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil)
|
||||
val ann = NodeAnnouncement(randomBytes64, Features.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil)
|
||||
nodeParams.db.network.addNode(ann)
|
||||
|
||||
val probe = TestProbe()
|
||||
|
@ -156,7 +158,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe
|
|||
connect(remoteNodeId, peer, peerConnection, channels = Set(ChannelCodecsSpec.normal))
|
||||
|
||||
probe.send(peer, Peer.Connect(remoteNodeId, None))
|
||||
probe.expectMsg("already connected")
|
||||
probe.expectMsg(PeerConnection.ConnectionResult.AlreadyConnected)
|
||||
}
|
||||
|
||||
test("handle disconnect in state CONNECTED") { f =>
|
||||
|
@ -258,7 +260,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe
|
|||
val probe = TestProbe()
|
||||
val fundingAmountBig = Btc(1).toSatoshi
|
||||
system.eventStream.subscribe(probe.ref, classOf[ChannelCreated])
|
||||
connect(remoteNodeId, peer, peerConnection, remoteInit = wire.Init(hex"80000")) // Bob supports wumbo
|
||||
connect(remoteNodeId, peer, peerConnection, remoteInit = wire.Init(Features(Set(ActivatedFeature(Wumbo, Optional))))) // Bob supports wumbo
|
||||
|
||||
assert(peer.stateData.channels.isEmpty)
|
||||
probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None))
|
||||
|
|
|
@ -52,7 +52,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
.modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true)
|
||||
|
||||
if (test.tags.contains("with_node_announcements")) {
|
||||
val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil)
|
||||
val bobAnnouncement = NodeAnnouncement(randomBytes64, Features.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil)
|
||||
aliceParams.db.network.addNode(bobAnnouncement)
|
||||
}
|
||||
|
||||
|
@ -164,7 +164,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
// we create a dummy tcp server and update bob's announcement to point to it
|
||||
val mockServer = new ServerSocket(0, 1, InetAddress.getLocalHost) // port will be assigned automatically
|
||||
val mockAddress = NodeAddress.fromParts(mockServer.getInetAddress.getHostAddress, mockServer.getLocalPort).get
|
||||
val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil)
|
||||
val bobAnnouncement = NodeAnnouncement(randomBytes64, Features.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil)
|
||||
nodeParams.db.network.addNode(bobAnnouncement)
|
||||
|
||||
val peer = TestProbe()
|
||||
|
|
|
@ -7,7 +7,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
|
|||
import fr.acinq.eclair.TestConstants._
|
||||
import fr.acinq.eclair.blockchain.TestWallet
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{NodeParams, TestKitBaseClass}
|
||||
import fr.acinq.eclair.{Features, NodeParams, TestKitBaseClass}
|
||||
import org.scalatest.funsuite.AnyFunSuiteLike
|
||||
import scodec.bits._
|
||||
|
||||
|
@ -36,7 +36,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
|
|||
val (probe, peer) = (TestProbe(), TestProbe())
|
||||
val remoteNodeId = PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")
|
||||
val remoteNodeAddress = NodeAddress.fromParts("127.0.0.1", 9735).get
|
||||
nodeParams.db.network.addNode(NodeAnnouncement(ByteVector64.Zeroes, ByteVector.empty, 0, remoteNodeId, Color(0, 0, 0), "alias", remoteNodeAddress :: Nil))
|
||||
nodeParams.db.network.addNode(NodeAnnouncement(ByteVector64.Zeroes, Features.empty, 0, remoteNodeId, Color(0, 0, 0), "alias", remoteNodeAddress :: Nil))
|
||||
|
||||
val switchboard = TestActorRef(new TestSwitchboard(nodeParams, remoteNodeId, peer))
|
||||
probe.send(switchboard, Peer.Connect(remoteNodeId, None))
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.Status.Failure
|
||||
import akka.testkit.{TestActorRef, TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{ByteVector32, Crypto}
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, PaymentSecret, VariableLengthOnion}
|
||||
import fr.acinq.eclair.TestConstants.Alice
|
||||
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC}
|
||||
import fr.acinq.eclair.db.IncomingPaymentStatus
|
||||
|
@ -30,10 +31,9 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart
|
|||
import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler}
|
||||
import fr.acinq.eclair.payment.relay.CommandBuffer
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomKey}
|
||||
import fr.acinq.eclair.{ActivatedFeature, CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomKey}
|
||||
import org.scalatest.Outcome
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
|
@ -43,9 +43,19 @@ import scala.concurrent.duration._
|
|||
|
||||
class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
|
||||
|
||||
val featuresWithMpp = Features(Set(
|
||||
ActivatedFeature(OptionDataLossProtect, Optional),
|
||||
ActivatedFeature(InitialRoutingSync, Optional),
|
||||
ActivatedFeature(ChannelRangeQueries, Optional),
|
||||
ActivatedFeature(ChannelRangeQueriesExtended, Optional),
|
||||
ActivatedFeature(VariableLengthOnion, Optional),
|
||||
ActivatedFeature(PaymentSecret, Optional),
|
||||
ActivatedFeature(BasicMultiPartPayment, Optional)
|
||||
))
|
||||
|
||||
case class FixtureParam(nodeParams: NodeParams, defaultExpiry: CltvExpiry, commandBuffer: TestProbe, eventListener: TestProbe, sender: TestProbe) {
|
||||
lazy val normalHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, commandBuffer.ref))
|
||||
lazy val mppHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = hex"028a8a"), commandBuffer.ref))
|
||||
lazy val mppHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = featuresWithMpp), commandBuffer.ref))
|
||||
}
|
||||
|
||||
override def withFixture(test: OneArgTest): Outcome = {
|
||||
|
@ -169,7 +179,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
}
|
||||
|
||||
{
|
||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = hex"028a8a"), TestProbe().ref))
|
||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref))
|
||||
sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.features.allowMultiPart)
|
||||
|
@ -185,7 +195,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
}
|
||||
|
||||
{
|
||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = hex"028a8a"), TestProbe().ref))
|
||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref))
|
||||
sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.features.allowMultiPart)
|
||||
|
@ -328,7 +338,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
}
|
||||
|
||||
test("PaymentHandler should handle multi-part payment timeout") { f =>
|
||||
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 200 millis, features = hex"028a8a")
|
||||
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 200 millis, features = featuresWithMpp)
|
||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref))
|
||||
|
||||
// Partial payment missing additional parts.
|
||||
|
@ -366,7 +376,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
}
|
||||
|
||||
test("PaymentHandler should handle multi-part payment success") { f =>
|
||||
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = hex"028a8a")
|
||||
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = featuresWithMpp)
|
||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref))
|
||||
|
||||
f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 fast coffee"))
|
||||
|
@ -412,7 +422,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
}
|
||||
|
||||
test("PaymentHandler should handle multi-part payment timeout then success") { f =>
|
||||
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 250 millis, features = hex"028a8a")
|
||||
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 250 millis, features = featuresWithMpp)
|
||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref))
|
||||
|
||||
f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 coffee, no sugar"))
|
||||
|
|
|
@ -24,7 +24,7 @@ import fr.acinq.bitcoin.{Block, Crypto}
|
|||
import fr.acinq.eclair.Features._
|
||||
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
|
||||
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
|
||||
import fr.acinq.eclair.payment.relay.{CommandBuffer, NodeRelayer, Origin, Relayer}
|
||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||
|
@ -335,7 +335,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
|
|||
|
||||
// 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.optional, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
|
||||
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(features))
|
||||
incomingMultiPart.foreach(incoming => relayer.send(nodeRelayer, incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload(
|
||||
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr
|
||||
|
@ -370,7 +370,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
|
|||
|
||||
// Receive an upstream multi-part payment.
|
||||
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(Features()))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(PaymentRequestFeatures()))
|
||||
incomingMultiPart.foreach(incoming => relayer.send(nodeRelayer, incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload(
|
||||
incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr
|
||||
))))
|
||||
|
|
|
@ -21,12 +21,13 @@ import java.util.UUID
|
|||
import akka.actor.ActorRef
|
||||
import akka.testkit.{TestActorRef, TestProbe}
|
||||
import fr.acinq.bitcoin.Block
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features._
|
||||
import fr.acinq.eclair.UInt64.Conversions._
|
||||
import fr.acinq.eclair.channel.{Channel, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentPacketSpec._
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
|
||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator._
|
||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
|
||||
|
@ -35,7 +36,7 @@ import fr.acinq.eclair.router.Router.{NodeHop, RouteParams}
|
|||
import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload}
|
||||
import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv}
|
||||
import fr.acinq.eclair.wire.{Onion, OnionCodecs, OnionTlv, TrampolineFeeInsufficient, _}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
|
||||
import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
@ -50,8 +51,20 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe)
|
||||
|
||||
val defaultTestFeatures = Features(Set(
|
||||
ActivatedFeature(InitialRoutingSync, Optional),
|
||||
ActivatedFeature(OptionDataLossProtect, Optional),
|
||||
ActivatedFeature(ChannelRangeQueries, Optional),
|
||||
ActivatedFeature(ChannelRangeQueriesExtended, Optional),
|
||||
ActivatedFeature(VariableLengthOnion, Optional)))
|
||||
|
||||
val featuresWithMpp = Features(
|
||||
defaultTestFeatures.activated +
|
||||
ActivatedFeature(PaymentSecret, Optional) +
|
||||
ActivatedFeature(BasicMultiPartPayment, Optional))
|
||||
|
||||
override def withFixture(test: OneArgTest): Outcome = {
|
||||
val features = if (test.tags.contains("mpp_disabled")) hex"0a8a" else hex"028a8a"
|
||||
val features = if (test.tags.contains("mpp_disabled")) defaultTestFeatures else featuresWithMpp
|
||||
val nodeParams = TestConstants.Alice.nodeParams.copy(features = features)
|
||||
val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe())
|
||||
val eventListener = TestProbe()
|
||||
|
@ -88,7 +101,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
test("reject payment with unknown mandatory feature") { f =>
|
||||
import f._
|
||||
val unknownFeature = 42
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(unknownFeature)))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(PaymentRequestFeatures(unknownFeature)))
|
||||
val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
|
@ -123,7 +136,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f =>
|
||||
import f._
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some MPP invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some MPP invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val req = SendPaymentRequest(finalAmount, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
|
@ -133,7 +146,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("forward multi-part payment") { f =>
|
||||
import f._
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
|
@ -143,7 +156,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("forward multi-part payment with pre-defined route") { f =>
|
||||
import f._
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val req = SendPaymentToRouteRequest(finalAmount / 2, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||
sender.send(initiator, req)
|
||||
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||
|
@ -157,7 +170,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("forward trampoline payment") { f =>
|
||||
import f._
|
||||
val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features), extraHops = ignoredRoutingHints)
|
||||
val trampolineFees = 21000 msat
|
||||
|
@ -229,7 +242,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
import f._
|
||||
// This is disabled because it would let the trampoline node steal the whole payment (if malicious).
|
||||
val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144))))
|
||||
val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)
|
||||
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)
|
||||
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, "#abittooreckless", None, None, routingHints, features = Some(features))
|
||||
val trampolineFees = 21000 msat
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9))
|
||||
|
@ -245,7 +258,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("retry trampoline payment") { f =>
|
||||
import f._
|
||||
val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features))
|
||||
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
|
||||
|
@ -275,7 +288,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("retry trampoline payment and fail") { f =>
|
||||
import f._
|
||||
val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features))
|
||||
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
|
||||
|
|
|
@ -55,6 +55,7 @@ import scala.concurrent.duration._
|
|||
class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||
|
||||
val defaultAmountMsat = 142000000 msat
|
||||
val defaultMaxFee = 4260000 msat // 3% of defaultAmountMsat
|
||||
val defaultExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(40000)
|
||||
val defaultPaymentPreimage = randomBytes32
|
||||
val defaultPaymentHash = Crypto.sha256(defaultPaymentPreimage)
|
||||
|
@ -62,6 +63,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val defaultExternalId = UUID.randomUUID().toString
|
||||
val defaultPaymentRequest = SendPaymentRequest(defaultAmountMsat, defaultPaymentHash, d, 1, externalId = Some(defaultExternalId))
|
||||
|
||||
def defaultRouteRequest(source: PublicKey, target: PublicKey): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee)
|
||||
|
||||
case class PaymentFixture(id: UUID,
|
||||
parentId: UUID,
|
||||
nodeParams: NodeParams,
|
||||
|
@ -149,7 +152,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsg(RouteRequest(c, d, defaultAmountMsat, ignoreNodes = Set(a, b)))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(c, d).copy(ignoreNodes = Set(a, b)))
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
|
||||
|
||||
|
@ -175,7 +178,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsg(RouteRequest(c, d, defaultAmountMsat, ignoreNodes = Set(a, b)))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(c, d).copy(ignoreNodes = Set(a, b)))
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
|
||||
|
||||
|
@ -183,7 +186,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
||||
sender.send(paymentFSM, UpdateFailHtlc(randomBytes32, 0, randomBytes(Sphinx.FailurePacket.PacketLength)))
|
||||
routerForwarder.expectMsg(RouteRequest(c, d, defaultAmountMsat, ignoreNodes = Set(a, b, c)))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(c, d).copy(ignoreNodes = Set(a, b, c)))
|
||||
val Transition(_, WAITING_FOR_PAYMENT_COMPLETE, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
assert(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
|
||||
}
|
||||
|
@ -223,7 +226,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(a, d))
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData
|
||||
|
@ -236,7 +239,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
sender.send(paymentFSM, Relayer.ForwardRemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32), defaultOrigin, UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket))) // unparsable message
|
||||
|
||||
// then the payment lifecycle will ask for a new route excluding all intermediate nodes
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, ignoreNodes = Set(c), ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(ignoreNodes = Set(c)))
|
||||
|
||||
// let's simulate a response by the router with another route
|
||||
sender.send(paymentFSM, RouteResponse(route :: Nil))
|
||||
|
@ -261,7 +264,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, _, _, _, _) = paymentFSM.stateData
|
||||
|
@ -270,7 +273,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, defaultPaymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None)))
|
||||
|
||||
// then the payment lifecycle will ask for a new route excluding the channel
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b))))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(ignoreChannels = Set(ChannelDesc(channelId_ab, a, b))))
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable
|
||||
}
|
||||
|
||||
|
@ -283,7 +286,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, _, _, _, _) = paymentFSM.stateData
|
||||
|
@ -292,7 +295,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32, FailureMessageCodecs.BADONION))
|
||||
|
||||
// then the payment lifecycle will ask for a new route excluding the channel
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b))))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(a, d).copy(ignoreChannels = Set(ChannelDesc(channelId_ab, a, b))))
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
}
|
||||
|
||||
|
@ -304,7 +307,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route) = paymentFSM.stateData
|
||||
|
@ -319,7 +322,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
// payment lifecycle forwards the embedded channelUpdate to the router
|
||||
routerForwarder.expectMsg(update_bc)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(a, d))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
// we allow 2 tries, so we send a 2nd request to the router
|
||||
assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(Nil, RouteNotFound) :: Nil)
|
||||
|
@ -334,7 +337,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route1) = paymentFSM.stateData
|
||||
|
@ -349,7 +352,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
// payment lifecycle forwards the embedded channelUpdate to the router
|
||||
routerForwarder.expectMsg(channelUpdate_bc_modified)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
|
||||
// router answers with a new route, taking into account the new update
|
||||
|
@ -369,7 +372,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
// but it will still forward the embedded channelUpdate to the router
|
||||
routerForwarder.expectMsg(channelUpdate_bc_modified_2)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
|
||||
// this time the router can't find a route: game over
|
||||
|
@ -392,7 +395,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = assistedRoutes, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(assistedRoutes = assistedRoutes))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, _) = paymentFSM.stateData
|
||||
|
@ -411,7 +414,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta),
|
||||
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
|
||||
))
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = assistedRoutes1, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(assistedRoutes = assistedRoutes1))
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
|
||||
// router answers with a new route, taking into account the new update
|
||||
|
@ -430,7 +433,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d))
|
||||
routerForwarder.forward(router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route1) = paymentFSM.stateData
|
||||
|
@ -440,7 +443,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
|
||||
// payment lifecycle forwards the embedded channelUpdate to the router
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_bc, b, c))))
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(ignoreChannels = Set(ChannelDesc(channelId_bc, b, c))))
|
||||
routerForwarder.forward(router)
|
||||
// we allow 2 tries, so we send a 2nd request to the router, which won't find another route
|
||||
|
||||
|
@ -492,7 +495,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
// \--(5)--> g
|
||||
val (priv_g, priv_funding_g) = (randomKey, randomKey)
|
||||
val (g, funding_g) = (priv_g.publicKey, priv_funding_g.publicKey)
|
||||
val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil, hex"0200")
|
||||
val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil, TestConstants.Bob.nodeParams.features)
|
||||
val channelId_bg = ShortChannelId(420000, 5, 0)
|
||||
val chan_bg = channelAnnouncement(channelId_bg, priv_b, priv_g, priv_funding_b, priv_funding_g)
|
||||
val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat)
|
||||
|
@ -505,7 +508,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_gb)
|
||||
watcher.expectMsg(ValidateRequest(chan_bg))
|
||||
watcher.send(router, ValidateResult(chan_bg, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_g)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))))
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
// on Android we only watch our channels
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
|
||||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
|
|
@ -21,17 +21,18 @@ import java.util.UUID
|
|||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet}
|
||||
import fr.acinq.eclair.FeatureSupport.Optional
|
||||
import fr.acinq.eclair.Features._
|
||||
import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.IncomingPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt}
|
||||
import fr.acinq.eclair.payment.OutgoingPacket._
|
||||
import fr.acinq.eclair.payment.PaymentRequest.Features
|
||||
import fr.acinq.eclair.payment.PaymentRequest.PaymentRequestFeatures
|
||||
import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop}
|
||||
import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload, RelayLegacyPayload}
|
||||
import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv, PaymentData}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, UInt64, nodeFee, randomBytes32, randomKey}
|
||||
import fr.acinq.eclair.{ActivatedFeature, CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, UInt64, nodeFee, randomBytes32, randomKey}
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.Attempt
|
||||
|
@ -69,11 +70,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(onion.packet.payload.length === Sphinx.PaymentPacket.PayloadLength)
|
||||
|
||||
// let's peel the onion
|
||||
val features = if (legacy) ByteVector.empty else variableLengthOnionFeature
|
||||
val features = if (legacy) Features.empty else variableLengthOnionFeature
|
||||
testPeelOnion(onion.packet, features)
|
||||
}
|
||||
|
||||
def testPeelOnion(packet_b: OnionRoutingPacket, features: ByteVector): Unit = {
|
||||
def testPeelOnion(packet_b: OnionRoutingPacket, features: Features): Unit = {
|
||||
val add_b = UpdateAddHtlc(randomBytes32, 0, amount_ab, paymentHash, expiry_ab, packet_b)
|
||||
val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, features)
|
||||
assert(add_b2 === add_b)
|
||||
|
@ -129,7 +130,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength)
|
||||
|
||||
// let's peel the onion
|
||||
testPeelOnion(add.onion, ByteVector.empty)
|
||||
testPeelOnion(add.onion, Features.empty)
|
||||
}
|
||||
|
||||
test("build a command with no hops") {
|
||||
|
@ -141,7 +142,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
|
||||
// let's peel the onion
|
||||
val add_b = UpdateAddHtlc(randomBytes32, 0, finalAmount, paymentHash, finalExpiry, add.onion)
|
||||
val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty)
|
||||
val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey, Features.empty)
|
||||
assert(add_b2 === add_b)
|
||||
assert(payload_b.amount === finalAmount)
|
||||
assert(payload_b.totalAmount === finalAmount)
|
||||
|
@ -164,7 +165,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(firstExpiry === expiry_ab)
|
||||
|
||||
val add_b = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
||||
val Right(ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty)
|
||||
assert(add_b2 === add_b)
|
||||
assert(payload_b === RelayLegacyPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc))
|
||||
|
||||
|
@ -215,7 +216,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
// a -> b -> c d -> e
|
||||
|
||||
val routingHints = List(List(PaymentRequest.ExtraHop(randomKey.publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144))))
|
||||
val invoiceFeatures = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)
|
||||
val invoiceFeatures = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)
|
||||
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, "#reckless", None, None, routingHints, features = Some(invoiceFeatures))
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, FinalLegacyPayload(finalAmount, finalExpiry))
|
||||
assert(amount_ac === amount_bc)
|
||||
|
@ -226,7 +227,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(firstExpiry === expiry_ab)
|
||||
|
||||
val add_b = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty)
|
||||
|
||||
val add_c = UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c)
|
||||
val Right(NodeRelayPacket(_, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey, variableLengthOnionFeature)
|
||||
|
@ -279,7 +280,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret))
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse)))
|
||||
val add_b = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty)
|
||||
val add_c = UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c)
|
||||
val Left(failure) = decrypt(add_c, priv_c.privateKey, variableLengthOnionFeature)
|
||||
assert(failure.isInstanceOf[InvalidOnionHmac])
|
||||
|
@ -295,28 +296,28 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
test("fail to decrypt when variable length onion is disabled") {
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry))))
|
||||
val add = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
||||
val Left(failure) = decrypt(add, priv_b.privateKey, ByteVector.empty) // tlv payload requires setting the variable-length onion feature bit
|
||||
val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) // tlv payload requires setting the variable-length onion feature bit
|
||||
assert(failure === InvalidRealm)
|
||||
}
|
||||
|
||||
test("fail to decrypt at the final node when amount has been modified by next-to-last node") {
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
|
||||
val add = UpdateAddHtlc(randomBytes32, 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet)
|
||||
val Left(failure) = decrypt(add, priv_b.privateKey, ByteVector.empty)
|
||||
val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty)
|
||||
assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat))
|
||||
}
|
||||
|
||||
test("fail to decrypt at the final node when expiry has been modified by next-to-last node") {
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
|
||||
val add = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet)
|
||||
val Left(failure) = decrypt(add, priv_b.privateKey, ByteVector.empty)
|
||||
val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty)
|
||||
assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12)))
|
||||
}
|
||||
|
||||
test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") {
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret))
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet))
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty)
|
||||
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature)
|
||||
// c forwards the trampoline payment to d.
|
||||
val (amount_d, expiry_d, onion_d) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, Onion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32, packet_d))
|
||||
|
@ -331,7 +332,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") {
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret))
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet))
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty)
|
||||
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature)
|
||||
// c forwards the trampoline payment to d.
|
||||
val (amount_d, expiry_d, onion_d) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, Onion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32, packet_d))
|
||||
|
@ -346,7 +347,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
test("fail to decrypt at the final trampoline node when payment secret is missing") {
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet))
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty)
|
||||
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature)
|
||||
// c forwards the trampoline payment to d.
|
||||
val (amount_d, expiry_d, onion_d) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, Onion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32, packet_d))
|
||||
|
@ -360,7 +361,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
test("fail to decrypt at intermediate trampoline node when amount is invalid") {
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet))
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty)
|
||||
// A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount.
|
||||
val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc - 100.msat, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature)
|
||||
assert(failure === FinalIncorrectHtlcAmount(amount_bc - 100.msat))
|
||||
|
@ -369,7 +370,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
test("fail to decrypt at intermediate trampoline node when expiry is invalid") {
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret
|
||||
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet))
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty)
|
||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty)
|
||||
// A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry.
|
||||
val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc - CltvExpiryDelta(12), packet_c), priv_c.privateKey, variableLengthOnionFeature)
|
||||
assert(failure === FinalIncorrectCltvExpiry(expiry_bc - CltvExpiryDelta(12)))
|
||||
|
@ -379,7 +380,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
|
||||
object PaymentPacketSpec {
|
||||
|
||||
val variableLengthOnionFeature = ByteVector.fromLong(1L << VariableLengthOnion.optional)
|
||||
val variableLengthOnionFeature = Features(Set(ActivatedFeature(VariableLengthOnion, Optional)))
|
||||
|
||||
/** Build onion from arbitrary tlv stream (potentially invalid). */
|
||||
def buildTlvOnion[T <: Onion.PacketType](packetType: Sphinx.OnionRoutingPacket[T])(nodes: Seq[PublicKey], payloads: Seq[TlvStream[OnionTlv]], associatedData: ByteVector32): OnionRoutingPacket = {
|
||||
|
|
|
@ -380,23 +380,23 @@ class PaymentRequestSpec extends AnyFunSuite {
|
|||
case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule"
|
||||
|
||||
val featureBits = Map(
|
||||
Features(bin" 00000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 00010100001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
|
||||
Features(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 01000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 0000010000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 0000011000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
Features(bin" 0000110000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
PaymentRequestFeatures(bin" 00000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 00010100001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 01000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 0000010000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 0000011000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
|
||||
PaymentRequestFeatures(bin" 0000110000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
// those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit)
|
||||
Features(bin" 0000100000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
Features(bin" 0010000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
Features(bin" 000001000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
Features(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
Features(bin"00000010000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
Features(bin"00001000000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false)
|
||||
PaymentRequestFeatures(bin" 0000100000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
PaymentRequestFeatures(bin" 0010000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
PaymentRequestFeatures(bin" 000001000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
PaymentRequestFeatures(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
PaymentRequestFeatures(bin"00000010000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
|
||||
PaymentRequestFeatures(bin"00001000000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false)
|
||||
)
|
||||
|
||||
for ((features, res) <- featureBits) {
|
||||
|
@ -419,14 +419,14 @@ class PaymentRequestSpec extends AnyFunSuite {
|
|||
)
|
||||
|
||||
for ((bitmask, featureBytes) <- testCases) {
|
||||
assert(Features(bitmask).toByteVector === featureBytes)
|
||||
assert(PaymentRequestFeatures(bitmask).toByteVector === featureBytes)
|
||||
}
|
||||
}
|
||||
|
||||
test("payment secret") {
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice")
|
||||
assert(pr.paymentSecret.isDefined)
|
||||
assert(pr.features === Features(PaymentSecret.optional, VariableLengthOnion.optional))
|
||||
assert(pr.features === PaymentRequestFeatures(PaymentSecret.optional, VariableLengthOnion.optional))
|
||||
assert(!pr.features.requirePaymentSecret)
|
||||
|
||||
val pr1 = PaymentRequest.read(PaymentRequest.write(pr))
|
||||
|
@ -453,11 +453,11 @@ class PaymentRequestSpec extends AnyFunSuite {
|
|||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice")
|
||||
assert(!pr.features.allowTrampoline)
|
||||
|
||||
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, TrampolinePayment.optional)))
|
||||
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, TrampolinePayment.optional)))
|
||||
assert(!pr1.features.allowMultiPart)
|
||||
assert(pr1.features.allowTrampoline)
|
||||
|
||||
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)))
|
||||
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)))
|
||||
assert(pr2.features.allowMultiPart)
|
||||
assert(pr2.features.allowTrampoline)
|
||||
|
||||
|
|
|
@ -27,11 +27,9 @@ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet
|
|||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, randomKey}
|
||||
import org.json4s
|
||||
import org.json4s.JsonAST.{JString, JValue}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, ShortChannelId, randomKey}
|
||||
import org.json4s.JsonAST.JString
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, ExecutionContext}
|
||||
|
@ -104,8 +102,8 @@ object AnnouncementsBatchValidationSpec {
|
|||
def makeChannelAnnouncement(c: SimulatedChannel)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): ChannelAnnouncement = {
|
||||
val (blockHeight, txIndex) = Await.result(extendedBitcoinClient.getTransactionShortId(c.fundingTx.txid), 10 seconds)
|
||||
val shortChannelId = ShortChannelId(blockHeight, txIndex, c.fundingOutputIndex)
|
||||
val (channelAnnNodeSig1, channelAnnBitcoinSig1) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key, c.node2Key.publicKey, c.node1FundingKey, c.node2FundingKey.publicKey, ByteVector.empty)
|
||||
val (channelAnnNodeSig2, channelAnnBitcoinSig2) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node2Key, c.node1Key.publicKey, c.node2FundingKey, c.node1FundingKey.publicKey, ByteVector.empty)
|
||||
val (channelAnnNodeSig1, channelAnnBitcoinSig1) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key, c.node2Key.publicKey, c.node1FundingKey, c.node2FundingKey.publicKey, Features.empty)
|
||||
val (channelAnnNodeSig2, channelAnnBitcoinSig2) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node2Key, c.node1Key.publicKey, c.node2FundingKey, c.node1FundingKey.publicKey, Features.empty)
|
||||
val channelAnnouncement = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key.publicKey, c.node2Key.publicKey, c.node1FundingKey.publicKey, c.node2FundingKey.publicKey, channelAnnNodeSig1, channelAnnNodeSig2, channelAnnBitcoinSig1, channelAnnBitcoinSig2)
|
||||
channelAnnouncement
|
||||
}
|
||||
|
|
|
@ -40,8 +40,8 @@ class AnnouncementsSpec extends AnyFunSuite {
|
|||
|
||||
ignore("create valid signed channel announcement") {
|
||||
val (node_a, node_b, bitcoin_a, bitcoin_b) = (randomKey, randomKey, randomKey, randomKey)
|
||||
val (node_a_sig, bitcoin_a_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a, node_b.publicKey, bitcoin_a, bitcoin_b.publicKey, ByteVector.empty)
|
||||
val (node_b_sig, bitcoin_b_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_b, node_a.publicKey, bitcoin_b, bitcoin_a.publicKey, ByteVector.empty)
|
||||
val (node_a_sig, bitcoin_a_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a, node_b.publicKey, bitcoin_a, bitcoin_b.publicKey, Features.empty)
|
||||
val (node_b_sig, bitcoin_b_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_b, node_a.publicKey, bitcoin_b, bitcoin_a.publicKey, Features.empty)
|
||||
val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig)
|
||||
assert(checkSigs(ann))
|
||||
assert(checkSigs(ann.copy(nodeId1 = randomKey.publicKey)) === false)
|
||||
|
@ -49,13 +49,13 @@ class AnnouncementsSpec extends AnyFunSuite {
|
|||
|
||||
ignore("create valid signed node announcement") {
|
||||
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.features)
|
||||
assert(Features.hasFeature(ann.features, Features.VariableLengthOnion))
|
||||
assert(ann.features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Optional)))
|
||||
assert(checkSig(ann))
|
||||
assert(checkSig(ann.copy(timestamp = 153)) === false)
|
||||
}
|
||||
|
||||
ignore("create valid signed channel update announcement") {
|
||||
val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey.publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimum, Alice.nodeParams.feeBase, Alice.nodeParams.feeProportionalMillionth, MilliSatoshi(500000000L))
|
||||
val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey.publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimum, Alice.nodeParams.feeBase, Alice.nodeParams.feeProportionalMillionth, 500000000 msat)
|
||||
assert(checkSig(ann, Alice.nodeParams.nodeId))
|
||||
assert(checkSig(ann, randomKey.publicKey) === false)
|
||||
}
|
||||
|
|
|
@ -61,13 +61,13 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi
|
|||
val (funding_a, funding_b, funding_c, funding_d, funding_e, funding_f, funding_g, funding_h) = (priv_funding_a.publicKey, priv_funding_b.publicKey, priv_funding_c.publicKey, priv_funding_d.publicKey, priv_funding_e.publicKey, priv_funding_f.publicKey, priv_funding_g.publicKey, priv_funding_h.publicKey)
|
||||
|
||||
// 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, hex"")
|
||||
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, hex"0200")
|
||||
val node_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, hex"00")
|
||||
val node_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, hex"00")
|
||||
val node_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, hex"00")
|
||||
val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, hex"00")
|
||||
val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, hex"00")
|
||||
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_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)
|
||||
val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, Features.empty)
|
||||
val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, Features.empty)
|
||||
|
||||
val channelId_ab = ShortChannelId(420000, 1, 0)
|
||||
val channelId_bc = ShortChannelId(420000, 2, 0)
|
||||
|
@ -79,8 +79,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi
|
|||
def shrink(u: ChannelUpdate) = u.copy(signature = null, chainHash = Block.RegtestGenesisBlock.hash)
|
||||
|
||||
def channelAnnouncement(shortChannelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = {
|
||||
val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, ByteVector.empty)
|
||||
val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, ByteVector.empty)
|
||||
val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, Features.empty)
|
||||
val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, Features.empty)
|
||||
makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig)
|
||||
}
|
||||
|
||||
|
@ -168,11 +168,12 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi
|
|||
watcher.send(router, ValidateResult(chan_ef, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(publicChannelCapacity, write(pay2wsh(Scripts.multiSig2of2(funding_e, funding_f)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))))
|
||||
watcher.send(router, ValidateResult(chan_gh, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(publicChannelCapacity, write(pay2wsh(Scripts.multiSig2of2(funding_g, funding_h)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))))
|
||||
// watcher receives watch-spent request
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
// On Android we only watch our channels
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
|
||||
// all messages are acked
|
||||
peerConnection.expectMsgAllOf(
|
||||
|
@ -216,8 +217,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi
|
|||
|
||||
object BaseRouterSpec {
|
||||
def channelAnnouncement(channelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = {
|
||||
val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, ByteVector.empty)
|
||||
val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, ByteVector.empty)
|
||||
val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, Features.empty)
|
||||
val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, Features.empty)
|
||||
makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,9 +26,9 @@ import fr.acinq.eclair.router.RouteCalculation._
|
|||
import fr.acinq.eclair.router.Router._
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, ToMilliSatoshiConversion, randomKey}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, ToMilliSatoshiConversion, randomKey}
|
||||
import org.scalatest.ParallelTestExecution
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits._
|
||||
|
||||
import scala.collection.immutable.SortedMap
|
||||
|
@ -52,18 +52,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 1 msat, 10, cltvDelta = CltvExpiryDelta(1))
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
}
|
||||
|
||||
test("check fee against max pct properly") {
|
||||
// fee is acceptable if it is either
|
||||
// - below our maximum fee base
|
||||
// - below our maximum fraction of the paid amount
|
||||
|
||||
// fee is acceptable if it is either:
|
||||
// - below our maximum fee base
|
||||
// - below our maximum fraction of the paid amount
|
||||
// here we have a maximum fee base of 1 msat, and all our updates have a base fee of 10 msat
|
||||
// so our fee will always be above the base fee, and we will always check that it is below our maximum percentage
|
||||
// of the amount being paid
|
||||
val routeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 1 msat)
|
||||
val maxFee = routeParams.getMaxFee(DEFAULT_AMOUNT_MSAT)
|
||||
|
||||
val g = DirectedGraph(List(
|
||||
makeEdge(1L, a, b, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)),
|
||||
|
@ -72,9 +73,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 10 msat, 10, cltvDelta = CltvExpiryDelta(1))
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 1 msat), currentBlockHeight = 400000)
|
||||
|
||||
assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, maxFee, numRoutes = 1, routeParams = routeParams, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
}
|
||||
|
||||
test("calculate the shortest path (correct fees)") {
|
||||
|
@ -116,7 +116,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(6L, f, d, feeBase = 1 msat, feeProportionalMillionth = 100, minHtlc = 0 msat)
|
||||
))
|
||||
|
||||
val Success(route) = findRoute(graph, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
val Success(Seq(route)) = findRoute(graph, a, d, amount, maxFee = 7 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
val weightedPath = Graph.pathWeight(a, route2Edges(route), amount, 0, None)
|
||||
assert(route2Ids(route) === 4 :: 5 :: 6 :: Nil)
|
||||
assert(weightedPath.length === 3)
|
||||
|
@ -127,7 +127,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
val graph2 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, capacity = 10 sat))
|
||||
val graph3 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, balance_opt = Some(10001 msat)))
|
||||
for (g <- Seq(graph1, graph2, graph3)) {
|
||||
val Success(route1) = findRoute(g, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
val Success(Seq(route1)) = findRoute(g, a, d, amount, maxFee = 10 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route1) === 1 :: 2 :: 3 :: Nil)
|
||||
}
|
||||
}
|
||||
|
@ -141,8 +141,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(5L, d, e, 5 msat, 0) // d -> e
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(2 :: 5 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 2 :: 5 :: Nil)
|
||||
}
|
||||
|
||||
test("calculate simple route (add and remove edges") {
|
||||
|
@ -153,12 +153,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
val Success(Seq(route1)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
|
||||
val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d))
|
||||
val route2 = findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route2 = findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2 === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("calculate the shortest path (hardcoded nodes)") {
|
||||
|
@ -176,8 +176,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, f, h, 50 msat, 0) // more expensive but fee will be ignored since f is the payer
|
||||
))
|
||||
|
||||
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(4 :: 3 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 4 :: 3 :: Nil)
|
||||
}
|
||||
|
||||
test("calculate the shortest path (select direct channel)") {
|
||||
|
@ -195,8 +195,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, h, i, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(4 :: Nil))
|
||||
val Success(Seq(route1, route2)) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route1) === 4 :: Nil)
|
||||
assert(route2Ids(route2) === 1 :: 2 :: 3 :: Nil)
|
||||
}
|
||||
|
||||
test("find a route using channels with htlMaximumMsat close to the payment amount") {
|
||||
|
@ -214,8 +215,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, h, i, 1 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) == Success(1 :: 2 :: 3 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 2 :: 3 :: Nil)
|
||||
}
|
||||
|
||||
test("find a route using channels with htlMinimumMsat close to the payment amount") {
|
||||
|
@ -233,8 +234,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, h, i, 1 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("if there are multiple channels between the same node, select the cheapest") {
|
||||
|
@ -252,8 +253,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, h, i, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(1 :: 6 :: 3 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 6 :: 3 :: Nil)
|
||||
}
|
||||
|
||||
test("if there are multiple channels between the same node, select one that has enough balance") {
|
||||
|
@ -271,8 +272,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, h, i, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 2 :: 3 :: Nil)
|
||||
}
|
||||
|
||||
test("calculate longer but cheaper route") {
|
||||
|
@ -284,8 +285,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(5L, b, e, 10 msat, 10)
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
}
|
||||
|
||||
test("no local channels") {
|
||||
|
@ -294,8 +295,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("route not found") {
|
||||
|
@ -305,8 +306,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("route not found (source OR target node not connected)") {
|
||||
|
@ -315,8 +316,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, c, d, 0 msat, 0)
|
||||
)).addVertex(a).addVertex(e)
|
||||
|
||||
assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("route not found (amount too high OR too low)") {
|
||||
|
@ -338,8 +339,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
val g = DirectedGraph(edgesHi)
|
||||
val g1 = DirectedGraph(edgesLo)
|
||||
|
||||
assert(findRoute(g, a, d, highAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
assert(findRoute(g1, a, d, lowAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("route not found (balance too low)") {
|
||||
|
@ -348,7 +349,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat),
|
||||
makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat)
|
||||
))
|
||||
assert(findRoute(g, a, d, 15000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).isSuccess)
|
||||
assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).isSuccess)
|
||||
|
||||
// not enough balance on the last edge
|
||||
val g1 = DirectedGraph(List(
|
||||
|
@ -368,7 +369,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat),
|
||||
makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat)
|
||||
))
|
||||
Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound)))
|
||||
Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound)))
|
||||
}
|
||||
|
||||
test("route to self") {
|
||||
|
@ -378,8 +379,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, c, d, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Failure(CannotRouteToSelf))
|
||||
val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route === Failure(CannotRouteToSelf))
|
||||
}
|
||||
|
||||
test("route to immediate neighbor") {
|
||||
|
@ -390,8 +391,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(1 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: Nil)
|
||||
}
|
||||
|
||||
test("directed graph") {
|
||||
|
@ -403,11 +404,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
val Success(Seq(route1))= findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
|
||||
val route2 = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route2 = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2 === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("calculate route and return metadata") {
|
||||
|
@ -434,8 +435,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
)
|
||||
|
||||
val g = DirectedGraph(edges)
|
||||
val hops = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).get.hops
|
||||
assert(hops === ChannelHop(a, b, uab) :: ChannelHop(b, c, ubc) :: ChannelHop(c, d, ucd) :: ChannelHop(d, e, ude) :: Nil)
|
||||
val Success(Seq(route)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.hops === ChannelHop(a, b, uab) :: ChannelHop(b, c, ubc) :: ChannelHop(c, d, ucd) :: ChannelHop(d, e, ude) :: Nil)
|
||||
}
|
||||
|
||||
test("convert extra hops to assisted channels") {
|
||||
|
@ -468,8 +469,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 0 msat, 0)
|
||||
))
|
||||
|
||||
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1 === Failure(RouteNotFound))
|
||||
|
||||
// verify that we left the graph untouched
|
||||
assert(g.containsEdge(ChannelDesc(ShortChannelId(3), c, d)))
|
||||
|
@ -477,8 +478,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
assert(g.containsVertex(d))
|
||||
|
||||
// make sure we can find a route if without the blacklist
|
||||
val route2 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
val Success(Seq(route2)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
}
|
||||
|
||||
test("route to a destination that is not in the graph (with assisted routes)") {
|
||||
|
@ -488,13 +489,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, c, d, 10 msat, 10)
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route === Failure(RouteNotFound))
|
||||
|
||||
// now we add the missing edge to reach the destination
|
||||
val extraGraphEdges = Set(makeEdge(4L, d, e, 5 msat, 5))
|
||||
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
val Success(Seq(route1)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
}
|
||||
|
||||
test("route from a source that is not in the graph (with assisted routes)") {
|
||||
|
@ -503,13 +504,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(3L, c, d, 10 msat, 10)
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Failure(RouteNotFound))
|
||||
val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route === Failure(RouteNotFound))
|
||||
|
||||
// now we add the missing starting edge
|
||||
val extraGraphEdges = Set(makeEdge(1L, a, b, 5 msat, 5))
|
||||
val route1 = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: Nil))
|
||||
val Success(Seq(route1)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route1) === 1 :: 2 :: 3 :: Nil)
|
||||
}
|
||||
|
||||
test("verify that extra hops takes precedence over known channels") {
|
||||
|
@ -520,14 +521,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(4L, d, e, 10 msat, 10)
|
||||
))
|
||||
|
||||
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
assert(route1.get.hops(1).lastUpdate.feeBaseMsat === 10.msat)
|
||||
val Success(Seq(route1)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
assert(route1.hops(1).lastUpdate.feeBaseMsat === 10.msat)
|
||||
|
||||
val extraGraphEdges = Set(makeEdge(2L, b, c, 5 msat, 5))
|
||||
val route2 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
|
||||
assert(route2.get.hops(1).lastUpdate.feeBaseMsat === 5.msat)
|
||||
val Success(Seq(route2)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
assert(route2.hops(1).lastUpdate.feeBaseMsat === 5.msat)
|
||||
}
|
||||
|
||||
test("compute ignored channels") {
|
||||
|
@ -583,10 +584,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
|
||||
val g = DirectedGraph(edges)
|
||||
|
||||
assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Success(0 until 18))
|
||||
assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Success(0 until 19))
|
||||
assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Success(0 until 20))
|
||||
assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Failure(RouteNotFound))
|
||||
assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(r => route2Ids(r.head)) === Success(0 until 18))
|
||||
assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(r => route2Ids(r.head)) === Success(0 until 19))
|
||||
assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(r => route2Ids(r.head)) === Success(0 until 20))
|
||||
assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("ignore cheaper route when it has more than 20 hops") {
|
||||
|
@ -601,8 +602,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
|
||||
val g = DirectedGraph(expensiveShortEdge :: edges)
|
||||
|
||||
val route = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 0 :: 1 :: 99 :: 48 :: Nil)
|
||||
}
|
||||
|
||||
test("ignore cheaper route when it has more than the requested CLTV") {
|
||||
|
@ -616,8 +617,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(6, f, d, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9))
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = CltvExpiryDelta(28)), currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(4 :: 5 :: 6 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = CltvExpiryDelta(28)), currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 4 :: 5 :: 6 :: Nil)
|
||||
}
|
||||
|
||||
test("ignore cheaper route when it grows longer than the requested size") {
|
||||
|
@ -631,8 +632,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(6, b, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9))
|
||||
))
|
||||
|
||||
val route = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3), currentBlockHeight = 400000)
|
||||
assert(route.map(route2Ids) === Success(1 :: 6 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3), currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 6 :: Nil)
|
||||
}
|
||||
|
||||
test("ignore loops") {
|
||||
|
@ -644,8 +645,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(5L, d, e, 10 msat, 10)
|
||||
))
|
||||
|
||||
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 2 :: 4 :: 5 :: Nil)
|
||||
}
|
||||
|
||||
test("ensure the route calculation terminates correctly when selecting 0-fees edges") {
|
||||
|
@ -660,8 +661,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(5L, e, d, 0 msat, 0) // e -> d
|
||||
))
|
||||
|
||||
val route1 = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route1.map(route2Ids) === Success(1 :: 3 :: 5 :: Nil))
|
||||
val Success(Seq(route)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Ids(route) === 1 :: 3 :: 5 :: Nil)
|
||||
}
|
||||
|
||||
// +---+ +---+ +---+
|
||||
|
@ -769,29 +770,31 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
}
|
||||
|
||||
test("select a random route below the requested fee") {
|
||||
val strictFeeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 7 msat, maxFeePct = 0)
|
||||
val strictFeeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 7 msat, maxFeePct = 0, randomize = true)
|
||||
val strictFee = strictFeeParams.getMaxFee(DEFAULT_AMOUNT_MSAT)
|
||||
assert(strictFee === 7.msat)
|
||||
|
||||
// A -> B -> C -> D has total cost of 10000005
|
||||
// A -> E -> C -> D has total cost of 11080003 !!
|
||||
// A -> E -> C -> D has total cost of 10000103 !!
|
||||
// A -> E -> F -> D has total cost of 10000006
|
||||
val g = DirectedGraph(List(
|
||||
makeEdge(1L, a, b, feeBase = 1 msat, 0),
|
||||
makeEdge(4L, a, e, feeBase = 1 msat, 0),
|
||||
makeEdge(2L, b, c, feeBase = 2 msat, 0),
|
||||
makeEdge(3L, c, d, feeBase = 3 msat, 0),
|
||||
makeEdge(4L, a, e, feeBase = 1 msat, 0),
|
||||
makeEdge(5L, e, f, feeBase = 3 msat, 0),
|
||||
makeEdge(6L, f, d, feeBase = 3 msat, 0),
|
||||
makeEdge(7L, e, c, feeBase = 9 msat, 0)
|
||||
makeEdge(7L, e, c, feeBase = 100 msat, 0)
|
||||
))
|
||||
|
||||
(for {_ <- 0 to 10} yield findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = 400000)).map {
|
||||
case Failure(thr) => fail(thr)
|
||||
case Success(someRoute) =>
|
||||
val weightedPath = Graph.pathWeight(a, route2Edges(someRoute), DEFAULT_AMOUNT_MSAT, 0, None)
|
||||
val totalFees = weightedPath.cost - DEFAULT_AMOUNT_MSAT
|
||||
// over the three routes we could only get the 2 cheapest because the third is too expensive (over 7 msat of fees)
|
||||
assert(totalFees === 5.msat || totalFees === 6.msat)
|
||||
assert(weightedPath.length === 3)
|
||||
for (_ <- 0 to 10) {
|
||||
val Success(routes) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, strictFee, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = 400000)
|
||||
assert(routes.length === 2, routes)
|
||||
val weightedPath = Graph.pathWeight(a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, 400000, None)
|
||||
val totalFees = weightedPath.cost - DEFAULT_AMOUNT_MSAT
|
||||
// over the three routes we could only get the 2 cheapest because the third is too expensive (over 7 msat of fees)
|
||||
assert(totalFees === 5.msat || totalFees === 6.msat)
|
||||
assert(weightedPath.length === 3)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -812,17 +815,17 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(7L, e, c, feeBase = 2 msat, 0, minHtlc = 0 msat, capacity = largeCapacity, cltvDelta = CltvExpiryDelta(12))
|
||||
))
|
||||
|
||||
val Success(routeFeeOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
val Success(Seq(routeFeeOptimized)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
|
||||
assert(route2Nodes(routeFeeOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil)
|
||||
|
||||
val Success(routeCltvOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
val Success(Seq(routeCltvOptimized)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
cltvDeltaFactor = 1,
|
||||
ageFactor = 0,
|
||||
capacityFactor = 0
|
||||
))), currentBlockHeight = 400000)
|
||||
assert(route2Nodes(routeCltvOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil)
|
||||
|
||||
val Success(routeCapacityOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
val Success(Seq(routeCapacityOptimized)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
cltvDeltaFactor = 0,
|
||||
ageFactor = 0,
|
||||
capacityFactor = 1
|
||||
|
@ -842,7 +845,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(ShortChannelId(s"${currentBlockHeight}x0x6").toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144))
|
||||
))
|
||||
|
||||
val Success(routeScoreOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
val Success(Seq(routeScoreOptimized)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
ageFactor = 0.33,
|
||||
cltvDeltaFactor = 0.33,
|
||||
capacityFactor = 0.33
|
||||
|
@ -861,7 +864,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12))
|
||||
))
|
||||
|
||||
val Success(routeScoreOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
val Success(Seq(routeScoreOptimized)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
ageFactor = 0.33,
|
||||
cltvDeltaFactor = 0.33,
|
||||
capacityFactor = 0.33
|
||||
|
@ -882,7 +885,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144))
|
||||
))
|
||||
|
||||
val Success(routeScoreOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
val Success(Seq(routeScoreOptimized)) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
|
||||
ageFactor = 0.33,
|
||||
cltvDeltaFactor = 0.33,
|
||||
capacityFactor = 0.33
|
||||
|
@ -929,7 +932,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
val targetNode = PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")
|
||||
val amount = 351000 msat
|
||||
|
||||
val Success(route) = findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = 567634) // simulate mainnet block for heuristic
|
||||
val Success(Seq(route)) = findRoute(g, thisNode, targetNode, amount, DEFAULT_MAX_FEE, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = 567634) // simulate mainnet block for heuristic
|
||||
assert(route.length == 2)
|
||||
assert(route.hops.last.nextNodeId == targetNode)
|
||||
}
|
||||
|
@ -961,6 +964,7 @@ object RouteCalculationSpec {
|
|||
val noopBoundaries = { _: RichWeight => true }
|
||||
|
||||
val DEFAULT_AMOUNT_MSAT = 10000000 msat
|
||||
val DEFAULT_MAX_FEE = 100000 msat
|
||||
val DEFAULT_CAPACITY = 100000 sat
|
||||
|
||||
val DEFAULT_ROUTE_PARAMS = RouteParams(randomize = false, maxFeeBase = 21000 msat, maxFeePct = 0.03, routeMaxCltv = CltvExpiryDelta(2016), routeMaxLength = 6, ratios = None)
|
||||
|
@ -969,7 +973,7 @@ object RouteCalculationSpec {
|
|||
|
||||
def makeChannel(shortChannelId: Long, nodeIdA: PublicKey, nodeIdB: PublicKey): ChannelAnnouncement = {
|
||||
val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeIdA, nodeIdB)) (nodeIdA, nodeIdB) else (nodeIdB, nodeIdA)
|
||||
ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, ByteVector.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey.publicKey, randomKey.publicKey)
|
||||
ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey.publicKey, randomKey.publicKey)
|
||||
}
|
||||
|
||||
def makeEdge(shortChannelId: Long,
|
||||
|
|
|
@ -27,14 +27,13 @@ import fr.acinq.eclair.crypto.TransportHandler
|
|||
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement}
|
||||
import fr.acinq.eclair.router.RouteCalculationSpec.DEFAULT_AMOUNT_MSAT
|
||||
import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE}
|
||||
import fr.acinq.eclair.router.Router._
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.wire.{Color, QueryShortChannelIds}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomKey}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, randomKey}
|
||||
import scodec.bits._
|
||||
|
||||
import scala.compat.Platform
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
|
@ -43,8 +42,6 @@ import scala.concurrent.duration._
|
|||
|
||||
class RouterSpec extends BaseRouterSpec {
|
||||
|
||||
val relaxedRouteParams = Some(RouteCalculationSpec.DEFAULT_ROUTE_PARAMS.copy(maxFeePct = 0.3))
|
||||
|
||||
test("properly announce valid new channels and ignore invalid ones") { fixture =>
|
||||
import fixture._
|
||||
val eventListener = TestProbe()
|
||||
|
@ -56,7 +53,7 @@ class RouterSpec extends BaseRouterSpec {
|
|||
// valid channel announcement, no stashing
|
||||
val chan_ac = channelAnnouncement(ShortChannelId(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, hex"0200", timestamp = System.currentTimeMillis.milliseconds.toSeconds + 1)
|
||||
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features, timestamp = System.currentTimeMillis.milliseconds.toSeconds + 1)
|
||||
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac))
|
||||
peerConnection.expectNoMsg(100 millis) // we don't immediately acknowledge the announcement (back pressure)
|
||||
watcher.expectMsg(ValidateRequest(chan_ac))
|
||||
|
@ -64,7 +61,8 @@ class RouterSpec extends BaseRouterSpec {
|
|||
peerConnection.expectMsg(TransportHandler.ReadAck(chan_ac))
|
||||
peerConnection.expectMsg(GossipDecision.Accepted(chan_ac))
|
||||
assert(peerConnection.sender() == router)
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
// On Android we only watch our channels
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ac))
|
||||
peerConnection.expectMsg(TransportHandler.ReadAck(update_ac))
|
||||
peerConnection.expectMsg(GossipDecision.Accepted(update_ac.copy(signature = null)))
|
||||
|
@ -87,7 +85,7 @@ class RouterSpec extends BaseRouterSpec {
|
|||
val priv_funding_u = randomKey
|
||||
val chan_uc = channelAnnouncement(ShortChannelId(420000, 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c)
|
||||
val update_uc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_u, c, chan_uc.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum)
|
||||
val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, hex"00")
|
||||
val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, Features.empty)
|
||||
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_uc))
|
||||
peerConnection.expectNoMsg(200 millis) // we don't immediately acknowledge the announcement (back pressure)
|
||||
watcher.expectMsg(ValidateRequest(chan_uc))
|
||||
|
@ -99,7 +97,8 @@ class RouterSpec extends BaseRouterSpec {
|
|||
peerConnection.expectMsg(TransportHandler.ReadAck(chan_uc))
|
||||
peerConnection.expectMsg(GossipDecision.Accepted(chan_uc))
|
||||
assert(peerConnection.sender() == router)
|
||||
watcher.expectMsgType[WatchSpentBasic]
|
||||
// On Android we only watch our channels
|
||||
// watcher.expectMsgType[WatchSpentBasic]
|
||||
peerConnection.expectMsg(GossipDecision.Accepted(update_uc.copy(signature = null)))
|
||||
peerConnection.expectMsg(GossipDecision.Accepted(node_u))
|
||||
eventListener.expectMsg(ChannelsDiscovered(SingleChannelDiscovered(chan_uc, 2000000 sat, None, None) :: Nil))
|
||||
|
@ -178,7 +177,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, hex"0200")
|
||||
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features)
|
||||
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay))
|
||||
peerConnection.expectMsg(TransportHandler.ReadAck(update_ay))
|
||||
peerConnection.expectMsg(GossipDecision.NoRelatedChannel(update_ay.copy(signature = null)))
|
||||
|
@ -196,7 +195,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, hex"0200")
|
||||
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features)
|
||||
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay))
|
||||
watcher.expectMsg(ValidateRequest(chan_ay))
|
||||
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay))
|
||||
|
@ -319,7 +318,7 @@ class RouterSpec extends BaseRouterSpec {
|
|||
import fixture._
|
||||
val sender = TestProbe()
|
||||
// no route a->f
|
||||
sender.send(router, RouteRequest(a, f, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -327,7 +326,7 @@ class RouterSpec extends BaseRouterSpec {
|
|||
import fixture._
|
||||
val sender = TestProbe()
|
||||
// no route a->f
|
||||
sender.send(router, RouteRequest(randomKey.publicKey, f, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(randomKey.publicKey, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -335,19 +334,19 @@ class RouterSpec extends BaseRouterSpec {
|
|||
import fixture._
|
||||
val sender = TestProbe()
|
||||
// no route a->f
|
||||
sender.send(router, RouteRequest(a, randomKey.publicKey, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(a, randomKey.publicKey, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("route found") { fixture =>
|
||||
import fixture._
|
||||
val sender = TestProbe()
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
val res = sender.expectMsgType[RouteResponse]
|
||||
assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil)
|
||||
assert(res.routes.head.hops.last.nextNodeId === d)
|
||||
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
val res1 = sender.expectMsgType[RouteResponse]
|
||||
assert(res1.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil)
|
||||
assert(res1.routes.head.hops.last.nextNodeId === h)
|
||||
|
@ -362,7 +361,7 @@ class RouterSpec extends BaseRouterSpec {
|
|||
val extraHop_cx = ExtraHop(c, ShortChannelId(1), 10 msat, 11, CltvExpiryDelta(12))
|
||||
val extraHop_xy = ExtraHop(x, ShortChannelId(2), 10 msat, 11, CltvExpiryDelta(12))
|
||||
val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20 msat, 21, CltvExpiryDelta(22))
|
||||
sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, assistedRoutes = Seq(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil)))
|
||||
sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, assistedRoutes = Seq(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil)))
|
||||
val res = sender.expectMsgType[RouteResponse]
|
||||
assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: x :: y :: Nil)
|
||||
assert(res.routes.head.hops.last.nextNodeId === z)
|
||||
|
@ -372,7 +371,7 @@ class RouterSpec extends BaseRouterSpec {
|
|||
import fixture._
|
||||
val sender = TestProbe()
|
||||
val peerConnection = TestProbe()
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
val res = sender.expectMsgType[RouteResponse]
|
||||
assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil)
|
||||
assert(res.routes.head.hops.last.nextNodeId === d)
|
||||
|
@ -380,21 +379,21 @@ class RouterSpec extends BaseRouterSpec {
|
|||
val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false)
|
||||
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1))
|
||||
peerConnection.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
test("route not found (private channel disabled)") { fixture =>
|
||||
import fixture._
|
||||
val sender = TestProbe()
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
val res = sender.expectMsgType[RouteResponse]
|
||||
assert(res.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil)
|
||||
assert(res.routes.head.hops.last.nextNodeId === h)
|
||||
|
||||
val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, channelId_ag, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false)
|
||||
sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ag, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false)))
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
}
|
||||
|
||||
|
@ -403,38 +402,38 @@ class RouterSpec extends BaseRouterSpec {
|
|||
val sender = TestProbe()
|
||||
|
||||
// Via private channels.
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsgType[RouteResponse]
|
||||
sender.send(router, RouteRequest(a, h, 50000000 msat))
|
||||
sender.send(router, RouteRequest(a, h, 50000000 msat, Long.MaxValue.msat))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
|
||||
// Via public channels.
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsgType[RouteResponse]
|
||||
val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true)
|
||||
sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments1))
|
||||
sender.send(router, RouteRequest(a, d, 12000000 msat))
|
||||
sender.send(router, RouteRequest(a, d, 12000000 msat, Long.MaxValue.msat))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
sender.send(router, RouteRequest(a, d, 5000000 msat))
|
||||
sender.send(router, RouteRequest(a, d, 5000000 msat, Long.MaxValue.msat))
|
||||
sender.expectMsgType[RouteResponse]
|
||||
}
|
||||
|
||||
test("temporary channel exclusion") { fixture =>
|
||||
import fixture._
|
||||
val sender = TestProbe()
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsgType[RouteResponse]
|
||||
val bc = ChannelDesc(channelId_bc, b, c)
|
||||
// let's exclude channel b->c
|
||||
sender.send(router, ExcludeChannel(bc))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsg(Failure(RouteNotFound))
|
||||
// note that cb is still available!
|
||||
sender.send(router, RouteRequest(d, a, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
|
||||
sender.send(router, RouteRequest(d, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsgType[RouteResponse]
|
||||
// let's remove the exclusion
|
||||
sender.send(router, LiftChannelExclusion(bc))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
|
||||
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
|
||||
sender.expectMsgType[RouteResponse]
|
||||
}
|
||||
|
||||
|
@ -521,24 +520,27 @@ class RouterSpec extends BaseRouterSpec {
|
|||
import fixture._
|
||||
|
||||
val sender = TestProbe()
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(channel_ab.meta_opt === None)
|
||||
// not supported on Android
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(channel_ab.meta_opt === None)
|
||||
|
||||
{
|
||||
// When the local channel comes back online, it will send a LocalChannelUpdate to the router.
|
||||
val balances = Set[Option[MilliSatoshi]](Some(10000 msat), Some(15000 msat))
|
||||
val commitments = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, b, announceChannel = true)
|
||||
sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments))
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances)
|
||||
// not supported on Android
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances)
|
||||
|
||||
// And the graph should be updated too.
|
||||
sender.send(router, Symbol("data"))
|
||||
val g = sender.expectMsgType[Data].graph
|
||||
val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get
|
||||
val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get
|
||||
// assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity)
|
||||
// assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity)
|
||||
assert(balances.contains(edge_ab.balance_opt))
|
||||
assert(edge_ba.balance_opt === None)
|
||||
}
|
||||
|
@ -553,15 +555,17 @@ class RouterSpec extends BaseRouterSpec {
|
|||
val balances = Set[Option[MilliSatoshi]](Some(11000 msat), Some(14000 msat))
|
||||
val commitments = CommitmentsSpec.makeCommitments(11000 msat, 14000 msat, a, b, announceChannel = true)
|
||||
sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments))
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances)
|
||||
// not supported on Android
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances)
|
||||
|
||||
// And the graph should be updated too.
|
||||
sender.send(router, Symbol("data"))
|
||||
val g = sender.expectMsgType[Data].graph
|
||||
val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get
|
||||
val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get
|
||||
// assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity)
|
||||
// assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity)
|
||||
assert(balances.contains(edge_ab.balance_opt))
|
||||
assert(edge_ba.balance_opt === None)
|
||||
}
|
||||
|
@ -571,15 +575,17 @@ class RouterSpec extends BaseRouterSpec {
|
|||
val balances = Set[Option[MilliSatoshi]](Some(12000 msat), Some(13000 msat))
|
||||
val commitments = CommitmentsSpec.makeCommitments(12000 msat, 13000 msat, a, b, announceChannel = true)
|
||||
sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ab, commitments))
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances)
|
||||
// not supported on Android
|
||||
// sender.send(router, GetRoutingState)
|
||||
// val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get
|
||||
// assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances)
|
||||
|
||||
// And the graph should be updated too.
|
||||
sender.send(router, Symbol("data"))
|
||||
val g = sender.expectMsgType[Data].graph
|
||||
val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get
|
||||
val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get
|
||||
// assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity)
|
||||
// assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity)
|
||||
assert(balances.contains(edge_ab.balance_opt))
|
||||
assert(edge_ba.balance_opt === None)
|
||||
}
|
||||
|
|
|
@ -328,8 +328,8 @@ 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(), hex"0200")
|
||||
val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "b", Color(0, 0, 0), List(), hex"00")
|
||||
val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features)
|
||||
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)
|
||||
}
|
||||
|
@ -345,7 +345,7 @@ object RoutingSyncSpec {
|
|||
|
||||
def makeFakeNodeAnnouncement(pub2priv: mutable.Map[PublicKey, PrivateKey])(nodeId: PublicKey): NodeAnnouncement = {
|
||||
val priv = pub2priv(nodeId)
|
||||
makeNodeAnnouncement(priv, "", Color(0, 0, 0), List(), hex"00")
|
||||
makeNodeAnnouncement(priv, "", Color(0, 0, 0), List(), Features.empty)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ class ChannelCodecsSpec extends AnyFunSuite {
|
|||
maxAcceptedHtlcs = Random.nextInt(Short.MaxValue),
|
||||
defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)),
|
||||
isFunder = Random.nextBoolean(),
|
||||
features = randomBytes(256))
|
||||
features = TestConstants.Alice.nodeParams.features)
|
||||
val encoded = localParamsCodec.encode(o).require
|
||||
val decoded = localParamsCodec.decode(encoded).require
|
||||
assert(o === decoded.value)
|
||||
|
@ -103,7 +103,7 @@ class ChannelCodecsSpec extends AnyFunSuite {
|
|||
// Backwards-compatibility: decode localparams with global features.
|
||||
val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500"
|
||||
val withGlobalFeaturesDecoded = localParamsCodec.decode(withGlobalFeatures.bits).require.value
|
||||
assert(withGlobalFeaturesDecoded.features === hex"0a8a")
|
||||
assert(withGlobalFeaturesDecoded.features.toByteVector === hex"0a8a")
|
||||
}
|
||||
|
||||
test("encode/decode remoteparams") {
|
||||
|
@ -120,7 +120,7 @@ class ChannelCodecsSpec extends AnyFunSuite {
|
|||
paymentBasepoint = randomKey.publicKey,
|
||||
delayedPaymentBasepoint = randomKey.publicKey,
|
||||
htlcBasepoint = randomKey.publicKey,
|
||||
features = randomBytes(256))
|
||||
features = TestConstants.Alice.nodeParams.features)
|
||||
val encoded = remoteParamsCodec.encode(o).require
|
||||
val decoded = remoteParamsCodec.decodeValue(encoded).require
|
||||
assert(o === decoded)
|
||||
|
@ -128,7 +128,7 @@ class ChannelCodecsSpec extends AnyFunSuite {
|
|||
// Backwards-compatibility: decode remoteparams with global features.
|
||||
val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a"
|
||||
val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value
|
||||
assert(withGlobalFeaturesDecoded.features === hex"028a")
|
||||
assert(withGlobalFeaturesDecoded.features.toByteVector === hex"028a")
|
||||
}
|
||||
|
||||
test("encode/decode htlc") {
|
||||
|
@ -365,6 +365,9 @@ class ChannelCodecsSpec extends AnyFunSuite {
|
|||
.replace(""""toRemote"""", """"toRemoteMsat"""")
|
||||
.replace("fundingKeyPath", "channelKeyPath")
|
||||
.replace(""""version":0,""", "")
|
||||
.replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"8a"""")
|
||||
.replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"81"""")
|
||||
.replace(""""features":{"activated":[],"unknown":[]}""", """"features":""""")
|
||||
|
||||
val newjson = Serialization.write(newnormal)(JsonSupport.formats)
|
||||
.replace(""","unknownFields":""""", "")
|
||||
|
@ -376,6 +379,9 @@ class ChannelCodecsSpec extends AnyFunSuite {
|
|||
.replace(""""toRemote"""", """"toRemoteMsat"""")
|
||||
.replace("fundingKeyPath", "channelKeyPath")
|
||||
.replace(""""version":0,""", "")
|
||||
.replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"8a"""")
|
||||
.replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"81"""")
|
||||
.replace(""""features":{"activated":[],"unknown":[]}""", """"features":""""")
|
||||
|
||||
assert(oldjson === refjson)
|
||||
assert(newjson === refjson)
|
||||
|
@ -396,7 +402,7 @@ object ChannelCodecsSpec {
|
|||
maxAcceptedHtlcs = 50,
|
||||
defaultFinalScriptPubKey = ByteVector.empty,
|
||||
isFunder = true,
|
||||
features = hex"deadbeef")
|
||||
features = Features.empty)
|
||||
|
||||
val remoteParams = RemoteParams(
|
||||
nodeId = randomKey.publicKey,
|
||||
|
@ -411,7 +417,7 @@ object ChannelCodecsSpec {
|
|||
paymentBasepoint = PrivateKey(ByteVector.fill(32)(3)).publicKey,
|
||||
delayedPaymentBasepoint = PrivateKey(ByteVector.fill(32)(4)).publicKey,
|
||||
htlcBasepoint = PrivateKey(ByteVector.fill(32)(6)).publicKey,
|
||||
features = hex"deadbeef")
|
||||
features = Features.empty)
|
||||
|
||||
val paymentPreimages = Seq(
|
||||
ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
|
|
|
@ -44,7 +44,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
|
|||
def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey
|
||||
|
||||
test("encode/decode init message") {
|
||||
case class TestCase(encoded: ByteVector, features: ByteVector, networks: List[ByteVector32], valid: Boolean, reEncoded: Option[ByteVector] = None)
|
||||
case class TestCase(encoded: ByteVector, rawFeatures: ByteVector, networks: List[ByteVector32], valid: Boolean, reEncoded: Option[ByteVector] = None)
|
||||
val chainHash1 = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101")
|
||||
val chainHash2 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202")
|
||||
val testCases = Seq(
|
||||
|
@ -67,7 +67,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
|
|||
for (testCase <- testCases) {
|
||||
if (testCase.valid) {
|
||||
val init = initCodec.decode(testCase.encoded.bits).require.value
|
||||
assert(init.features === testCase.features)
|
||||
assert(init.features.toByteVector === testCase.rawFeatures)
|
||||
assert(init.networks === testCase.networks)
|
||||
val encoded = initCodec.encode(init).require
|
||||
assert(encoded.bytes === testCase.reEncoded.getOrElse(testCase.encoded))
|
||||
|
@ -173,8 +173,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
|
|||
val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32, 2, randomBytes32, 1111)
|
||||
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, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
|
||||
val node_announcement = NodeAnnouncement(randomBytes64, bin(1, 2), 1, 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_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, 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, 42, 0, 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, 1500)
|
||||
|
|
|
@ -17,40 +17,8 @@ kamon.instrumentation.akka {
|
|||
}
|
||||
|
||||
akka {
|
||||
|
||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||
logger-startup-timeout = 30s
|
||||
loglevel = "DEBUG" # akka doc: You can enable DEBUG level for akka.loglevel and control the actual level in the SLF4J backend without any significant overhead, also for production.
|
||||
logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
|
||||
|
||||
io {
|
||||
tcp {
|
||||
|
||||
# The maximum number of bytes delivered by a `Received` message. Before
|
||||
# more data is read from the network the connection actor will try to
|
||||
# do other work.
|
||||
# The purpose of this setting is to impose a smaller limit than the
|
||||
# configured receive buffer size. When using value 'unlimited' it will
|
||||
# try to read all from the receive buffer.
|
||||
# As per BOLT#8 lightning messages are at most 2 + 16 + 65535 + 16 = 65569bytes
|
||||
# Currently the largest message is update_add_htlc (~1500b).
|
||||
# As a tradeoff to reduce the RAM consumption, in conjunction with tcp pull mode,
|
||||
# the default value is chosen to allow for a decent number of messages to be prefetched.
|
||||
max-received-message-size = 16384b
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
# Default maximum content length which should not be exceeded by incoming request entities.
|
||||
# Can be changed at runtime (to a higher or lower value) via the `HttpEntity::withSizeLimit` method.
|
||||
# Note that it is not necessarily a problem to set this to a high value as all stream operations
|
||||
# are always properly backpressured.
|
||||
# Nevertheless you might want to apply some limit in order to prevent a single client from consuming
|
||||
# an excessive amount of server resources.
|
||||
#
|
||||
# Set to `infinite` to completely disable entity length checks. (Even then you can still apply one
|
||||
# programmatically via `withSizeLimit`.)
|
||||
#
|
||||
# We disable the size check, because the batching bitcoin json-rpc client may return very large results
|
||||
http.client.parsing.max-content-length=infinite
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2012, Joshua Suereth
|
||||
# All rights reserved.
|
||||
#
|
||||
|
@ -7,8 +9,6 @@
|
|||
# Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#!/usr/bin/env bash
|
||||
|
||||
### ------------------------------- ###
|
||||
### Helper methods for BASH scripts ###
|
||||
### ------------------------------- ###
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2012, Joshua Suereth
|
||||
# All rights reserved.
|
||||
#
|
||||
|
@ -7,8 +9,6 @@
|
|||
# Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#!/usr/bin/env bash
|
||||
|
||||
### ------------------------------- ###
|
||||
### Helper methods for BASH scripts ###
|
||||
### ------------------------------- ###
|
||||
|
|
|
@ -32,20 +32,20 @@ import scala.util.{Failure, Success}
|
|||
* Created by PM on 25/01/2016.
|
||||
*/
|
||||
object Boot extends App with Logging {
|
||||
|
||||
val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair"))
|
||||
|
||||
try {
|
||||
val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair"))
|
||||
val config = NodeParams.loadConfiguration(datadir)
|
||||
|
||||
val plugins = Plugin.loadPlugins(args.map(new File(_)))
|
||||
plugins.foreach(plugin => logger.info(s"loaded plugin ${plugin.getClass.getSimpleName}"))
|
||||
implicit val system: ActorSystem = ActorSystem("eclair-node")
|
||||
implicit val system: ActorSystem = ActorSystem("eclair-node", config)
|
||||
implicit val ec: ExecutionContext = system.dispatcher
|
||||
val setup = new Setup(datadir)
|
||||
|
||||
plugins.foreach(_.onSetup(setup))
|
||||
setup.bootstrap onComplete {
|
||||
case Success(kit) =>
|
||||
startApiServiceIfEnabled(setup.config, kit)
|
||||
startApiServiceIfEnabled(kit)
|
||||
plugins.foreach(_.onKit(kit))
|
||||
case Failure(t) => onError(t)
|
||||
}
|
||||
|
@ -56,12 +56,12 @@ object Boot extends App with Logging {
|
|||
/**
|
||||
* Starts the http APIs service if enabled in the configuration
|
||||
*
|
||||
* @param config
|
||||
* @param kit
|
||||
* @param system
|
||||
* @param ec
|
||||
*/
|
||||
def startApiServiceIfEnabled(config: Config, kit: Kit)(implicit system: ActorSystem, ec: ExecutionContext) = {
|
||||
def startApiServiceIfEnabled(kit: Kit)(implicit system: ActorSystem, ec: ExecutionContext) = {
|
||||
val config = system.settings.config.getConfig("eclair")
|
||||
if(config.getBoolean("api.enabled")){
|
||||
logger.info(s"json API enabled on port=${config.getInt("api.port")}")
|
||||
val apiPassword = config.getString("api.password") match {
|
||||
|
|
|
@ -30,7 +30,7 @@ import fr.acinq.eclair.router.Router.RouteResponse
|
|||
import fr.acinq.eclair.transactions.DirectedHtlc
|
||||
import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64}
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
|
||||
import org.json4s.JsonAST._
|
||||
import org.json4s.{CustomKeySerializer, CustomSerializer, DefaultFormats, Extraction, TypeHints, jackson}
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -227,6 +227,7 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( {
|
|||
val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq
|
||||
val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq
|
||||
val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq
|
||||
val features = JField("features", JsonSupport.featuresToJson(Features(p.features.bitmask)))
|
||||
val fieldList = List(JField("prefix", JString(p.prefix)),
|
||||
JField("timestamp", JLong(p.timestamp)),
|
||||
JField("nodeId", JString(p.nodeId.toString())),
|
||||
|
@ -238,10 +239,17 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( {
|
|||
JField("paymentHash", JString(p.paymentHash.toString()))) ++
|
||||
expiry ++
|
||||
minFinalCltvExpiry ++
|
||||
amount
|
||||
amount :+
|
||||
features
|
||||
JObject(fieldList)
|
||||
}))
|
||||
|
||||
class FeaturesSerializer extends CustomSerializer[Features](_ => ( {
|
||||
null
|
||||
}, {
|
||||
case features: Features => JsonSupport.featuresToJson(features)
|
||||
}))
|
||||
|
||||
class JavaUUIDSerializer extends CustomSerializer[UUID](_ => ( {
|
||||
null
|
||||
}, {
|
||||
|
@ -281,7 +289,8 @@ object JsonSupport extends Json4sJacksonSupport {
|
|||
new NodeAddressSerializer +
|
||||
new DirectedHtlcSerializer +
|
||||
new PaymentRequestSerializer +
|
||||
new JavaUUIDSerializer
|
||||
new JavaUUIDSerializer +
|
||||
new FeaturesSerializer
|
||||
|
||||
case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints {
|
||||
val reverse: Map[String, Class[_]] = custom.map(_.swap)
|
||||
|
@ -293,4 +302,15 @@ object JsonSupport extends Json4sJacksonSupport {
|
|||
override def classFor(hint: String): Option[Class[_]] = reverse.get(hint)
|
||||
}
|
||||
|
||||
def featuresToJson(features: Features) = JObject(
|
||||
JField("activated", JArray(features.activated.map { a =>
|
||||
JObject(
|
||||
JField("name", JString(a.feature.rfcName)),
|
||||
JField("support", JString(a.support.toString))
|
||||
)}.toList)),
|
||||
JField("unknown", JArray(features.unknown.map { i =>
|
||||
JObject(
|
||||
JField("featureBit", JInt(i.bitIndex))
|
||||
)}.toList))
|
||||
)
|
||||
}
|
|
@ -1 +1 @@
|
|||
{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":"","chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":9999,"publicAddresses":["localhost:9731"]}
|
||||
{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":[{"name":"option_data_loss_protect","support":"mandatory"},{"name":"gossip_queries_ex","support":"optional"}],"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":9999,"publicAddresses":["localhost:9731"]}
|
|
@ -1 +1 @@
|
|||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"expired"}}
|
||||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":[],"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"expired"}}
|
|
@ -1 +1 @@
|
|||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"pending"}}
|
||||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":[],"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"pending"}}
|
|
@ -1 +1 @@
|
|||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}}
|
||||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":[],"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}}
|
|
@ -21,6 +21,7 @@ import java.util.UUID
|
|||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32}
|
||||
import fr.acinq.eclair.Features.{ChannelRangeQueriesExtended, OptionDataLossProtect}
|
||||
import fr.acinq.eclair.channel.ChannelCommandResponse.ChannelClosed
|
||||
import fr.acinq.eclair.io.NodeURI
|
||||
import fr.acinq.eclair.io.Peer.PeerInfo
|
||||
|
@ -176,7 +177,7 @@ class ApiServiceSpec extends AnyFunSuiteLike with ScalatestRouteTest with RouteT
|
|||
publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil,
|
||||
version = "1.0.0-SNAPSHOT-e3f1ec0",
|
||||
color = "#000102",
|
||||
features = ""
|
||||
features = Features(Set(ActivatedFeature(OptionDataLossProtect, FeatureSupport.Mandatory), ActivatedFeature(ChannelRangeQueriesExtended, FeatureSupport.Optional)))
|
||||
))
|
||||
|
||||
Post("/getinfo") ~>
|
||||
|
|
|
@ -84,9 +84,9 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers {
|
|||
}
|
||||
|
||||
test("Payment Request") {
|
||||
val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
||||
val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
JsonSupport.serialization.write(pr)(JsonSupport.json4sJacksonFormats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000}"""
|
||||
JsonSupport.serialization.write(pr)(JsonSupport.json4sJacksonFormats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":[{"name":"var_onion_optin","support":"optional"},{"name":"payment_secret","support":"optional"}],"unknown":[]}}"""
|
||||
}
|
||||
|
||||
test("type hints") {
|
||||
|
|
Loading…
Add table
Reference in a new issue