1
0
Fork 0
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:
sstone 2020-05-20 14:50:08 +02:00
commit d77e9664c9
71 changed files with 1040 additions and 655 deletions

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -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,

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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)

View file

@ -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()

View file

@ -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")
}
})

View file

@ -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) ::

View file

@ -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")
}

View file

@ -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.

View file

@ -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)

View file

@ -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
}

View file

@ -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.

View file

@ -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])

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -19,7 +19,7 @@ package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.wire._
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)

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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,

View file

@ -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) ::

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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))
}

View file

@ -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)

View file

@ -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,

View file

@ -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,

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)
}

View file

@ -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))

View file

@ -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()

View file

@ -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))

View file

@ -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"))

View file

@ -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
))))

View file

@ -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))

View file

@ -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._

View file

@ -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 = {

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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,

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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"),

View file

@ -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)

View file

@ -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
}

View file

@ -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 ###
### ------------------------------- ###

View file

@ -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 ###
### ------------------------------- ###

View file

@ -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 {

View file

@ -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))
)
}

View file

@ -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"]}

View file

@ -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"}}

View file

@ -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"}}

View file

@ -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}}

View file

@ -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") ~>

View file

@ -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") {