diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8a3831eae6..533664d9b4 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -37,14 +37,12 @@ eclair { node-color = "49daaa" trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag - global-features = "0200" // variable_length_onion - local-features = "088a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex + features = "0a8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex + variable_length_onion override-features = [ // optional per-node features - # { - # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - # global-features = "", - # local-features = "" - # } + # { + # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + # features = "" + # } ] sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers channel-flags = 1 // announce channels @@ -132,7 +130,7 @@ eclair { // the values below will be used to perform route searching path-finding { max-route-length = 6 // max route length for the 'first pass', if none is found then a second pass is made with no limit - max-cltv = 1008 // max acceptable cltv expiry for the payment (1008 ~ 1 week) + max-cltv = 1008 // max acceptable cltv expiry for the payment (1008 ~ 1 week) fee-threshold-sat = 21 // if fee is below this value we skip the max-fee-pct check max-fee-pct = 0.03 // route will be discarded if fee is above this value (in percentage relative to the total payment amount); doesn't apply if fee < fee-threshold-sat diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 931de580dc..0a54d58be8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -30,11 +30,11 @@ 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.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest} -import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment -import fr.acinq.eclair.router.{Announcements, ChannelDesc, GetNetworkStats, NetworkStats, PublicChannel, RouteRequest, RouteResponse, Router} +import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance} +import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest} +import fr.acinq.eclair.router._ import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import scodec.bits.ByteVector @@ -80,7 +80,7 @@ trait Eclair { def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] - def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], allowMultiPart: Boolean)(implicit timeout: Timeout): Future[PaymentRequest] + def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] def newAddress(): Future[String] @@ -190,7 +190,7 @@ class EclairImpl(appKit: Kit) extends Eclair { override def allUpdates(nodeId_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[ChannelUpdate]] = nodeId_opt match { case None => (appKit.router ? 'updates).mapTo[Iterable[ChannelUpdate]] case Some(pk) => (appKit.router ? 'channelsMap).mapTo[Map[ShortChannelId, PublicChannel]].map { channels => - channels.map(_._2).flatMap { + channels.values.flatMap { case PublicChannel(ann, _, _, Some(u1), _) if ann.nodeId1 == pk && u1.isNode1 => List(u1) case PublicChannel(ann, _, _, _, Some(u2)) if ann.nodeId2 == pk && !u2.isNode1 => List(u2) case PublicChannel(_, _, _, _, _) => List.empty @@ -198,9 +198,9 @@ class EclairImpl(appKit: Kit) extends Eclair { } } - override def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], allowMultiPart: Boolean)(implicit timeout: Timeout): Future[PaymentRequest] = { + override def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] = { fallbackAddress_opt.map { fa => fr.acinq.eclair.addressToPublicKeyScript(fa, appKit.nodeParams.chainHash) } // if it's not a bitcoin address throws an exception - (appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt, allowMultiPart = allowMultiPart)).mapTo[PaymentRequest] + (appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest] } override def newAddress(): Future[String] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 6ea498592a..7f9ea52bd1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -19,63 +19,119 @@ package fr.acinq.eclair import scodec.bits.{BitVector, ByteVector} /** - * Created by PM on 13/02/2017. - */ + * Created by PM on 13/02/2017. + */ + +sealed trait FeatureSupport + +// @formatter:off +object FeatureSupport { + case object Mandatory extends FeatureSupport + case object Optional extends FeatureSupport +} +// @formatter:on + +sealed trait Feature { + def rfcName: String + + def mandatory: Int + + def optional: Int = mandatory + 1 + + override def toString = rfcName +} + object Features { - val OPTION_DATA_LOSS_PROTECT_MANDATORY = 0 - val OPTION_DATA_LOSS_PROTECT_OPTIONAL = 1 - // reserved but not used as per lightningnetwork/lightning-rfc/pull/178 - //val INITIAL_ROUTING_SYNC_BIT_MANDATORY = 2 - val INITIAL_ROUTING_SYNC_BIT_OPTIONAL = 3 + case object OptionDataLossProtect extends Feature { + val rfcName = "option_data_loss_protect" + val mandatory = 0 + } - val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 - val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 + case object InitialRoutingSync extends Feature { + val rfcName = "initial_routing_sync" + // reserved but not used as per lightningnetwork/lightning-rfc/pull/178 + val mandatory = 2 + } - val VARIABLE_LENGTH_ONION_MANDATORY = 8 - val VARIABLE_LENGTH_ONION_OPTIONAL = 9 + case object ChannelRangeQueries extends Feature { + val rfcName = "gossip_queries" + val mandatory = 6 + } + + case object VariableLengthOnion extends Feature { + val rfcName = "var_onion_optin" + val mandatory = 8 + } - val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 10 - val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 11 + case object ChannelRangeQueriesExtended extends Feature { + val rfcName = "gossip_queries_ex" + val mandatory = 10 + } - val PAYMENT_SECRET_MANDATORY = 14 - val PAYMENT_SECRET_OPTIONAL = 15 + case object PaymentSecret extends Feature { + val rfcName = "payment_secret" + val mandatory = 14 + } - val BASIC_MULTI_PART_PAYMENT_MANDATORY = 16 - val BASIC_MULTI_PART_PAYMENT_OPTIONAL = 17 + case object BasicMultiPartPayment extends Feature { + val rfcName = "basic_mpp" + val mandatory = 16 + } // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) - // We're not advertizing these bits yet in our announcements, clients have to assume support. + // We're not advertising these bits yet in our announcements, clients have to assume support. // This is why we haven't added them yet to `areSupported`. - val TRAMPOLINE_PAYMENT_MANDATORY = 50 - val TRAMPOLINE_PAYMENT_OPTIONAL = 51 + case object TrampolinePayment extends Feature { + val rfcName = "trampoline_payment" + val mandatory = 50 + } + + // Features may depend on other features, as specified in Bolt 9. + private val featuresDependency = Map( + ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil), + PaymentSecret -> (VariableLengthOnion :: Nil), + BasicMultiPartPayment -> (PaymentSecret :: Nil), + TrampolinePayment -> (PaymentSecret :: Nil) + ) + + 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. - def hasFeature(features: BitVector, bit: Int): Boolean = if (features.sizeLessThanOrEqual(bit)) false else features.reverse.get(bit) + private def hasFeature(features: BitVector, bit: Int): Boolean = features.sizeGreaterThan(bit) && features.reverse.get(bit) - def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(features.bits, 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) + } - /** - * We currently don't distinguish mandatory and optional. Interpreting VARIABLE_LENGTH_ONION_MANDATORY strictly would - * be very restrictive and probably fork us out of the network. - * We may implement this distinction later, but for now both flags are interpreted as an optional support. - */ - def hasVariableLengthOnion(features: ByteVector): Boolean = hasFeature(features, VARIABLE_LENGTH_ONION_MANDATORY) || hasFeature(features, VARIABLE_LENGTH_ONION_OPTIONAL) + 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). - */ + * 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[Long]( - OPTION_DATA_LOSS_PROTECT_MANDATORY, - CHANNEL_RANGE_QUERIES_BIT_MANDATORY, - VARIABLE_LENGTH_ONION_MANDATORY, - CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY, - PAYMENT_SECRET_MANDATORY, - BASIC_MULTI_PART_PAYMENT_MANDATORY) + val supportedMandatoryFeatures = Set( + OptionDataLossProtect, + ChannelRangeQueries, + VariableLengthOnion, + ChannelRangeQueriesExtended, + PaymentSecret, + BasicMultiPartPayment + ).map(_.mandatory.toLong) val reversed = features.reverse for (i <- 0L until reversed.length by 2) { if (reversed.get(i) && !supportedMandatoryFeatures.contains(i)) return false @@ -85,9 +141,9 @@ object Features { } /** - * A feature set is supported if all even bits are supported. - * We just ignore unknown odd bits. - */ + * 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) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index ffdea41762..ef70452cd1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -46,9 +46,8 @@ case class NodeParams(keyManager: KeyManager, alias: String, color: Color, publicAddresses: List[NodeAddress], - globalFeatures: ByteVector, - localFeatures: ByteVector, - overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)], + features: ByteVector, + overrideFeatures: Map[PublicKey, ByteVector], syncWhitelist: Set[PublicKey], dustLimit: Satoshi, onChainFeeConf: OnChainFeeConf, @@ -84,6 +83,7 @@ case class NodeParams(keyManager: KeyManager, enableTrampolinePayment: Boolean) { val privateKey = keyManager.nodeKey.privateKey val nodeId = keyManager.nodeId + def currentBlockHeight: Long = blockCount.get } @@ -130,11 +130,15 @@ object NodeParams { } def makeNodeParams(config: Config, keyManager: KeyManager, torAddress_opt: Option[NodeAddress], database: Databases, blockCount: AtomicLong, feeEstimator: FeeEstimator): NodeParams = { - // check configuration for keys that have been renamed in v0.3.2 + // check configuration for keys that have been renamed val deprecatedKeyPaths = Map( + // v0.3.2 "default-feerates" -> "on-chain-fees.default-feerates", "max-feerate-mismatch" -> "on-chain-fees.max-feerate-mismatch", - "update-fee_min-diff-ratio" -> "on-chain-fees.update-fee-min-diff-ratio" + "update-fee_min-diff-ratio" -> "on-chain-fees.update-fee-min-diff-ratio", + // v0.3.3 + "global-features" -> "features", + "local-features" -> "features" ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -170,11 +174,14 @@ object NodeParams { val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") - val overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)] = config.getConfigList("override-features").map { e => + val features = ByteVector.fromValidHex(config.getString("features")) + val featuresErr = Features.validateFeatureGraph(features) + require(featuresErr.isEmpty, featuresErr.map(_.message)) + + val overrideFeatures: Map[PublicKey, ByteVector] = config.getConfigList("override-features").map { e => val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) - val gf = ByteVector.fromValidHex(e.getString("global-features")) - val lf = ByteVector.fromValidHex(e.getString("local-features")) - p -> (gf, lf) + val f = ByteVector.fromValidHex(e.getString("features")) + p -> f }.toMap val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").map(s => PublicKey(ByteVector.fromValidHex(s))).toSet @@ -219,8 +226,7 @@ object NodeParams { alias = nodeAlias, color = Color(color(0), color(1), color(2)), publicAddresses = addresses, - globalFeatures = ByteVector.fromValidHex(config.getString("global-features")), - localFeatures = ByteVector.fromValidHex(config.getString("local-features")), + features = features, overrideFeatures = overrideFeatures, syncWhitelist = syncWhitelist, dustLimit = dustLimitSatoshis, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index e495778fac..2dff44edfe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -313,8 +313,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId paymentBasepoint = open.paymentBasepoint, delayedPaymentBasepoint = open.delayedPaymentBasepoint, htlcBasepoint = open.htlcBasepoint, - globalFeatures = remoteInit.globalFeatures, - localFeatures = remoteInit.localFeatures) + features = remoteInit.features) log.debug(s"remote params: $remoteParams") goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, channelVersion, accept) sending accept } @@ -346,8 +345,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId paymentBasepoint = accept.paymentBasepoint, delayedPaymentBasepoint = accept.delayedPaymentBasepoint, htlcBasepoint = accept.htlcBasepoint, - globalFeatures = remoteInit.globalFeatures, - localFeatures = remoteInit.localFeatures) + features = remoteInit.features) log.debug(s"remote params: $remoteParams") val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 2dc6fcf026..a88a8b5bbf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -212,8 +212,7 @@ final case class LocalParams(nodeId: PublicKey, maxAcceptedHtlcs: Int, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, - globalFeatures: ByteVector, - localFeatures: ByteVector) + features: ByteVector) final case class RemoteParams(nodeId: PublicKey, dustLimit: Satoshi, @@ -227,8 +226,7 @@ final case class RemoteParams(nodeId: PublicKey, paymentBasepoint: PublicKey, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, - globalFeatures: ByteVector, - localFeatures: ByteVector) + features: ByteVector) object ChannelFlags { val AnnounceChannel = 0x01.toByte diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 12eab85066..0f06861c1d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -67,8 +67,8 @@ object Helpers { */ def updateFeatures(data: HasCommitments, localInit: Init, remoteInit: Init): HasCommitments = { val commitments1 = data.commitments.copy( - localParams = data.commitments.localParams.copy(globalFeatures = localInit.globalFeatures, localFeatures = localInit.localFeatures), - remoteParams = data.commitments.remoteParams.copy(globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures)) + localParams = data.commitments.localParams.copy(features = localInit.features), + remoteParams = data.commitments.remoteParams.copy(features = remoteInit.features)) data match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1) case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1) @@ -173,11 +173,11 @@ object Helpers { } /** - * - * @param referenceFeePerKw reference fee rate per kiloweight - * @param currentFeePerKw current fee rate per kiloweight - * @return the "normalized" difference between i.e local and remote fee rate: |reference - current| / avg(current, reference) - */ + * + * @param referenceFeePerKw reference fee rate per kiloweight + * @param currentFeePerKw current fee rate per kiloweight + * @return the "normalized" difference between i.e local and remote fee rate: |reference - current| / avg(current, reference) + */ def feeRateMismatch(referenceFeePerKw: Long, currentFeePerKw: Long): Double = Math.abs((2.0 * (referenceFeePerKw - currentFeePerKw)) / (currentFeePerKw + referenceFeePerKw)) @@ -185,13 +185,13 @@ object Helpers { feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio /** - * - * @param referenceFeePerKw reference fee rate per kiloweight - * @param currentFeePerKw current fee rate per kiloweight - * @param maxFeerateMismatchRatio maximum fee rate mismatch ratio - * @return true if the difference between current and reference fee rates is too high. - * the actual check is |reference - current| / avg(current, reference) > mismatch ratio - */ + * + * @param referenceFeePerKw reference fee rate per kiloweight + * @param currentFeePerKw current fee rate per kiloweight + * @param maxFeerateMismatchRatio maximum fee rate mismatch ratio + * @return true if the difference between current and reference fee rates is too high. + * the actual check is |reference - current| / avg(current, reference) > mismatch ratio + */ def isFeeDiffTooHigh(referenceFeePerKw: Long, currentFeePerKw: Long, maxFeerateMismatchRatio: Double): Boolean = feeRateMismatch(referenceFeePerKw, currentFeePerKw) > maxFeerateMismatchRatio @@ -459,7 +459,7 @@ object Helpers { // TODO: check that val dustLimitSatoshis = localParams.dustLimit.max(remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) val closingSigned = ClosingSigned(channelId, closingFee, localClosingSig) log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index c721d7e002..cdf1390449 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -23,12 +23,11 @@ import akka.event.Logging.MDC import akka.util.Timeout import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto, DeterministicWallet, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, Satoshi} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} @@ -38,7 +37,7 @@ import scodec.bits.ByteVector import scala.compat.Platform import scala.concurrent.duration._ -import scala.util.{Failure, Random, Success, Try} +import scala.util.Random /** * Created by PM on 26/08/2016. @@ -102,10 +101,10 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A transport ! TransportHandler.Listener(self) context watch transport val localInit = nodeParams.overrideFeatures.get(remoteNodeId) match { - case Some((gf, lf)) => wire.Init(globalFeatures = gf, localFeatures = lf) - case None => wire.Init(globalFeatures = nodeParams.globalFeatures, localFeatures = nodeParams.localFeatures) + case Some(f) => wire.Init(f) + case None => wire.Init(nodeParams.features) } - log.info(s"using globalFeatures=${localInit.globalFeatures.toBin} and localFeatures=${localInit.localFeatures.toBin}") + log.info(s"using features=${localInit.features.toBin}") transport ! localInit val address_opt = if (outgoing) { @@ -135,21 +134,17 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A case Event(remoteInit: wire.Init, d: InitializingData) => d.transport ! TransportHandler.ReadAck(remoteInit) - log.info(s"peer is using globalFeatures=${remoteInit.globalFeatures.toBin} and localFeatures=${remoteInit.localFeatures.toBin}") + log.info(s"peer is using features=${remoteInit.features.toBin}") - if (Features.areSupported(remoteInit.localFeatures)) { + if (Features.areSupported(remoteInit.features)) { d.origin_opt.foreach(origin => origin ! "connected") - import Features._ + def localHasFeature(f: Feature): Boolean = Features.hasFeature(d.localInit.features, f) - def hasLocalFeature(bit: Int) = Features.hasFeature(d.localInit.localFeatures, bit) - - def hasRemoteFeature(bit: Int) = Features.hasFeature(remoteInit.localFeatures, bit) - - val canUseChannelRangeQueries = (hasLocalFeature(CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) || hasLocalFeature(CHANNEL_RANGE_QUERIES_BIT_MANDATORY)) && (hasRemoteFeature(CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) || hasRemoteFeature(CHANNEL_RANGE_QUERIES_BIT_MANDATORY)) - - val canUseChannelRangeQueriesEx = (hasLocalFeature(CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) || hasLocalFeature(CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY)) && (hasRemoteFeature(CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) || hasRemoteFeature(CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY)) + def remoteHasFeature(f: Feature): Boolean = Features.hasFeature(remoteInit.features, f) + val canUseChannelRangeQueries = localHasFeature(Features.ChannelRangeQueries) && remoteHasFeature(Features.ChannelRangeQueries) + val canUseChannelRangeQueriesEx = localHasFeature(Features.ChannelRangeQueriesExtended) && remoteHasFeature(Features.ChannelRangeQueriesExtended) if (canUseChannelRangeQueries || canUseChannelRangeQueriesEx) { // if they support channel queries we don't send routing info yet, if they want it they will query us // we will query them, using extended queries if supported @@ -160,7 +155,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A } else { log.info("not syncing with this peer") } - } else if (hasRemoteFeature(INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) { + } else if (remoteHasFeature(Features.InitialRoutingSync)) { // "old" nodes, do as before log.info("peer requested a full routing table dump") router ! GetRoutingState @@ -548,7 +543,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A } onTermination { - case StopEvent(_, CONNECTED, d: ConnectedData) => + case StopEvent(_, CONNECTED, _: ConnectedData) => // the transition handler won't be fired if we go directly from CONNECTED to closed Metrics.connectedPeers.decrement() context.system.eventStream.publish(PeerDisconnected(self, remoteNodeId)) @@ -685,8 +680,7 @@ object Peer { maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, defaultFinalScriptPubKey = defaultFinalScriptPubKey, isFunder = isFunder, - globalFeatures = nodeParams.globalFeatures, - localFeatures = nodeParams.localFeatures) + features = nodeParams.features) } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 2f58a77a11..33994b9c9e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -54,11 +54,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: ByteVector)(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.hasVariableLengthOnion(features) => Left(InvalidRealm) + case Attempt.Successful(DecodeResult(_: Onion.TlvFormat, _)) if !Features.hasFeature(features, Features.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") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala index 2eaf85b4b1..69ad5d3379 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala @@ -18,9 +18,9 @@ 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._ +import fr.acinq.eclair.Features.{PaymentSecret => PaymentSecretF, _} import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32} import scodec.Codec import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} @@ -45,9 +45,8 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam amount.foreach(a => require(a > 0.msat, s"amount is not valid")) require(tags.collect { case _: PaymentRequest.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") require(tags.collect { case PaymentRequest.Description(_) | PaymentRequest.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") - if (features.allowMultiPart) { - require(features.allowPaymentSecret, "there must be a payment secret when using multi-part") - } + private val featuresErr = validateFeatureGraph(features.bitmask) + require(featuresErr.isEmpty, featuresErr.map(_.message)) if (features.allowPaymentSecret) { require(tags.collect { case _: PaymentRequest.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") } @@ -130,7 +129,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(PAYMENT_SECRET_OPTIONAL))): PaymentRequest = { + features: Option[Features] = Some(Features(VariableLengthOnion.optional, PaymentSecretF.optional))): PaymentRequest = { val prefix = prefixes(chainHash) val tags = { @@ -330,10 +329,10 @@ object PaymentRequest { */ case class Features(bitmask: BitVector) extends TaggedField { lazy val supported: Boolean = areSupported(bitmask) - lazy val allowMultiPart: Boolean = hasFeature(bitmask, BASIC_MULTI_PART_PAYMENT_MANDATORY) || hasFeature(bitmask, BASIC_MULTI_PART_PAYMENT_OPTIONAL) - lazy val allowPaymentSecret: Boolean = hasFeature(bitmask, PAYMENT_SECRET_MANDATORY) || hasFeature(bitmask, PAYMENT_SECRET_OPTIONAL) - lazy val requirePaymentSecret: Boolean = hasFeature(bitmask, PAYMENT_SECRET_MANDATORY) - lazy val allowTrampoline: Boolean = hasFeature(bitmask, TRAMPOLINE_PAYMENT_MANDATORY) || hasFeature(bitmask, TRAMPOLINE_PAYMENT_OPTIONAL) + 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) override def toString: String = s"Features(${bitmask.toBin})" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 3dd1d67f97..f98b6878f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -54,7 +54,7 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu def onSuccess(paymentReceived: PaymentReceived)(implicit log: LoggingAdapter): Unit = () override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = { - case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt, paymentPreimage_opt, allowMultiPart) => + case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt, paymentPreimage_opt) => Try { val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32) val paymentHash = Crypto.sha256(paymentPreimage) @@ -62,9 +62,10 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu // We currently only optionally support payment secrets (to allow legacy clients to pay invoices). // Once we're confident most of the network has upgraded, we should switch to mandatory payment secrets. val features = { - val f1 = Seq(Features.PAYMENT_SECRET_OPTIONAL) - val f2 = if (allowMultiPart) Seq(Features.BASIC_MULTI_PART_PAYMENT_OPTIONAL) else Nil - val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TRAMPOLINE_PAYMENT_OPTIONAL) else Nil + val f1 = Seq(Features.PaymentSecret.optional, Features.VariableLengthOnion.optional) + val allowMultiPart = Features.hasFeature(nodeParams.features, 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: _*)) } val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features) @@ -162,15 +163,13 @@ object MultiPartHandler { * @param extraHops routing hints to help the payer. * @param fallbackAddress fallback Bitcoin address. * @param paymentPreimage payment preimage. - * @param allowMultiPart allow multi-part payments. */ case class ReceivePayment(amount_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None, - paymentPreimage: Option[ByteVector32] = None, - allowMultiPart: Boolean = false) + paymentPreimage: Option[ByteVector32] = None) private def validatePaymentStatus(payment: IncomingPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Boolean = { if (record.status.isInstanceOf[IncomingPaymentStatus.Received]) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 58d822c4b2..f8a8519f56 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -63,7 +63,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in // Closed channels will be removed, other channels will be restored. val channels = nodeParams.db.channels.listLocalChannels().filter(c => Closing.isClosed(c, None).isEmpty) cleanupRelayDb(channels, nodeParams.db.pendingRelay) - checkBrokenHtlcs(channels, nodeParams.db.payments, nodeParams.privateKey, nodeParams.globalFeatures) + checkBrokenHtlcs(channels, nodeParams.db.payments, nodeParams.privateKey, nodeParams.features) } override def receive: Receive = main(brokenHtlcs) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index bffbf38aa1..bce752def2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -122,7 +122,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm case ForwardAdd(add, previousFailures) => log.debug(s"received forwarding request for htlc #${add.id} from channelId=${add.channelId}") - IncomingPacket.decrypt(add, nodeParams.privateKey, nodeParams.globalFeatures) match { + IncomingPacket.decrypt(add, nodeParams.privateKey, nodeParams.features) match { case Right(p: IncomingPacket.FinalPacket) => log.debug(s"forwarding htlc #${add.id} to payment-handler") paymentHandler forward p diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 10e13ab434..24faf2633d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -246,7 +246,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.globalFeatures) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features) self ! nodeAnn log.info(s"computing network stats...") @@ -363,7 +363,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ // in case we just validated our first local channel, we announce the local node if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) { log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.globalFeatures) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features) self ! nodeAnn } Some(PublicChannel(c, tx.txid, capacity, None, None)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 6e8126caec..f520680627 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -70,8 +70,7 @@ object ChannelCodecs extends Logging { ("maxAcceptedHtlcs" | uint16) :: ("isFunder" | bool) :: ("defaultFinalScriptPubKey" | varsizebinarydata) :: - ("globalFeatures" | varsizebinarydata) :: - ("localFeatures" | varsizebinarydata)).as[LocalParams] + ("features" | combinedFeaturesCodec)).as[LocalParams] val remoteParamsCodec: Codec[RemoteParams] = ( ("nodeId" | publicKey) :: @@ -86,8 +85,7 @@ object ChannelCodecs extends Logging { ("paymentBasepoint" | publicKey) :: ("delayedPaymentBasepoint" | publicKey) :: ("htlcBasepoint" | publicKey) :: - ("globalFeatures" | varsizebinarydata) :: - ("localFeatures" | varsizebinarydata)).as[RemoteParams] + ("features" | combinedFeaturesCodec)).as[RemoteParams] val directionCodec: Codec[Direction] = Codec[Direction]( (dir: Direction) => bool.encode(dir == IN), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 408c2b8a69..2e030cc110 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -16,22 +16,30 @@ package fr.acinq.eclair.wire -import fr.acinq.eclair.{KamonExt, wire} import fr.acinq.eclair.wire.CommonCodecs._ +import fr.acinq.eclair.{KamonExt, wire} import kamon.Kamon import kamon.tag.TagSet -import scodec.bits.BitVector -import scodec.{Attempt, Codec} +import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ +import scodec.{Attempt, Codec} /** * Created by PM on 15/11/2016. */ object LightningMessageCodecs { - val initCodec: Codec[Init] = ( + /** 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] = ( ("globalFeatures" | varsizebinarydata) :: - ("localFeatures" | varsizebinarydata)).as[Init] + ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[ByteVector]( + { case (gf, lf) => + val length = gf.length.max(lf.length) + gf.padLeft(length) | lf.padLeft(length) + }, + { features => (ByteVector.empty, features) }) + + val initCodec: Codec[Init] = combinedFeaturesCodec.as[Init] val errorCodec: Codec[Error] = ( ("channelId" | bytes32) :: @@ -239,10 +247,10 @@ object LightningMessageCodecs { ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: ("tlvStream" | QueryChannelRangeTlv.codec) - ).as[QueryChannelRange] + ).as[QueryChannelRange] } - val replyChannelRangeCodec: Codec[ReplyChannelRange] = { + val replyChannelRangeCodec: Codec[ReplyChannelRange] = { Codec( ("chainHash" | bytes32) :: ("firstBlockNum" | uint32) :: @@ -250,7 +258,7 @@ object LightningMessageCodecs { ("complete" | byte) :: ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: ("tlvStream" | ReplyChannelRangeTlv.codec) - ).as[ReplyChannelRange] + ).as[ReplyChannelRange] } val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index e3d8af8214..45ae3c69b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -45,11 +45,9 @@ sealed trait HasChainHash extends LightningMessage { def chainHash: ByteVector32 sealed trait UpdateMessage extends HtlcMessage // <- not in the spec // @formatter:on -case class Init(globalFeatures: ByteVector, - localFeatures: ByteVector) extends SetupMessage +case class Init(features: ByteVector) extends SetupMessage -case class Error(channelId: ByteVector32, - data: ByteVector) extends SetupMessage with HasChannelId { +case class Error(channelId: ByteVector32, data: ByteVector) extends SetupMessage with HasChannelId { def toAscii: String = if (fr.acinq.eclair.isAsciiPrintable(data)) new String(data.toArray, StandardCharsets.US_ASCII) else "n/a" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 6d23200f09..95f46a3fed 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -204,18 +204,18 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL }) } - test("router returns Network Stats") { f=> + test("router returns Network Stats") { f => import f._ - val capStat=Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat) - val cltvStat=Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53)) - val feeBaseStat=Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat) - val feePropStat=Stats(32L, 11L, 13L, 22L, 42L, 51L, 53L) + val capStat = Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat) + val cltvStat = Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53)) + val feeBaseStat = Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat) + val feePropStat = Stats(32L, 11L, 13L, 22L, 42L, 51L, 53L) val eclair = new EclairImpl(kit) val fResp = eclair.networkStats() f.router.expectMsg(GetNetworkStats) - f.router.reply(Some(new NetworkStats(1,2,capStat,cltvStat,feeBaseStat,feePropStat))) + f.router.reply(Some(new NetworkStats(1, 2, capStat, cltvStat, feeBaseStat, feePropStat))) awaitCond({ fResp.value match { @@ -252,16 +252,15 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL val fallBackAddressRaw = "muhtvdmsnbQEPFuEmxcChX58fGvXaaUoVt" val eclair = new EclairImpl(kit) - eclair.receive("some desc", Some(123 msat), Some(456), Some(fallBackAddressRaw), None, allowMultiPart = true) + eclair.receive("some desc", Some(123 msat), Some(456), Some(fallBackAddressRaw), None) val receive = paymentHandler.expectMsgType[ReceivePayment] assert(receive.amount_opt === Some(123 msat)) assert(receive.expirySeconds_opt === Some(456)) assert(receive.fallbackAddress === Some(fallBackAddressRaw)) - assert(receive.allowMultiPart) // try with wrong address format - assertThrows[IllegalArgumentException](eclair.receive("some desc", Some(123 msat), Some(456), Some("wassa wassa"), None, allowMultiPart = false)) + assertThrows[IllegalArgumentException](eclair.receive("some desc", Some(123 msat), Some(456), Some("wassa wassa"), None)) } test("passing a payment_preimage to /createinvoice should result in an invoice with payment_hash=H(payment_preimage)") { f => @@ -271,7 +270,7 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL val eclair = new EclairImpl(kitWithPaymentHandler) val paymentPreimage = randomBytes32 - val fResp = eclair.receive("some desc", None, None, None, Some(paymentPreimage), allowMultiPart = false) + val fResp = eclair.receive("some desc", None, None, None, Some(paymentPreimage)) awaitCond({ fResp.value match { case Some(Success(pr)) => pr.paymentHash == Crypto.sha256(paymentPreimage) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 92355590a5..efa1cd205c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -21,46 +21,84 @@ import org.scalatest.FunSuite import scodec.bits._ /** - * Created by PM on 27/01/2017. - */ + * Created by PM on 27/01/2017. + */ class FeaturesSpec extends FunSuite { test("'initial_routing_sync' feature") { - assert(hasFeature(hex"08", Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) + assert(hasFeature(hex"08", InitialRoutingSync, Some(FeatureSupport.Optional))) + assert(!hasFeature(hex"08", InitialRoutingSync, Some(FeatureSupport.Mandatory))) } test("'data_loss_protect' feature") { - assert(hasFeature(hex"01", Features.OPTION_DATA_LOSS_PROTECT_MANDATORY)) - assert(hasFeature(hex"02", Features.OPTION_DATA_LOSS_PROTECT_OPTIONAL)) + assert(hasFeature(hex"01", OptionDataLossProtect, Some(FeatureSupport.Mandatory))) + assert(hasFeature(hex"02", OptionDataLossProtect, Some(FeatureSupport.Optional))) } test("'initial_routing_sync', 'data_loss_protect' and 'variable_length_onion' features") { val features = hex"010a" - assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL) && hasFeature(features, VARIABLE_LENGTH_ONION_MANDATORY)) + assert(areSupported(features)) + assert(hasFeature(features, OptionDataLossProtect)) + assert(hasFeature(features, InitialRoutingSync, None)) + assert(hasFeature(features, VariableLengthOnion)) } test("'variable_length_onion' feature") { - assert(hasFeature(hex"0100", Features.VARIABLE_LENGTH_ONION_MANDATORY)) - assert(hasVariableLengthOnion(hex"0100")) - assert(hasFeature(hex"0200", Features.VARIABLE_LENGTH_ONION_OPTIONAL)) - assert(hasVariableLengthOnion(hex"0200")) + 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))) + } + + test("features dependencies") { + val testCases = Map( + bin" " -> true, + bin" 00000000" -> true, + bin" 01011000" -> true, + // gossip_queries_ex depend on gossip_queries + bin"000000000000100000000000" -> false, + bin"000000000000010000000000" -> false, + bin"000000000000100010000000" -> true, + bin"000000000000100001000000" -> true, + // payment_secret depends on var_onion_optin + bin"000000001000000000000000" -> false, + bin"000000000100000000000000" -> false, + bin"000000000100001000000000" -> true, + // basic_mpp depends on payment_secret + bin"000000100000000000000000" -> false, + bin"000000010000000000000000" -> false, + bin"000000101000000000000000" -> false, + bin"000000011000000000000000" -> false, + bin"000000011000001000000000" -> true, + bin"000000100100000100000000" -> true + ) + + for ((testCase, valid) <- testCases) { + if (valid) { + assert(validateFeatureGraph(testCase) === None) + assert(validateFeatureGraph(testCase.bytes) === None) + } else { + assert(validateFeatureGraph(testCase).nonEmpty) + assert(validateFeatureGraph(testCase.bytes).nonEmpty) + } + } } test("features compatibility") { - assert(areSupported(ByteVector.fromLong(1L << INITIAL_ROUTING_SYNC_BIT_OPTIONAL))) - assert(areSupported(ByteVector.fromLong(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY))) - assert(areSupported(ByteVector.fromLong(1L << OPTION_DATA_LOSS_PROTECT_OPTIONAL))) - assert(areSupported(ByteVector.fromLong(1L << CHANNEL_RANGE_QUERIES_BIT_MANDATORY))) - assert(areSupported(ByteVector.fromLong(1L << CHANNEL_RANGE_QUERIES_BIT_OPTIONAL))) - assert(areSupported(ByteVector.fromLong(1L << VARIABLE_LENGTH_ONION_OPTIONAL))) - assert(areSupported(ByteVector.fromLong(1L << VARIABLE_LENGTH_ONION_MANDATORY))) - assert(areSupported(ByteVector.fromLong(1L << CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY))) - assert(areSupported(ByteVector.fromLong(1L << CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL))) - assert(areSupported(ByteVector.fromLong(1L << PAYMENT_SECRET_MANDATORY))) - assert(areSupported(ByteVector.fromLong(1L << PAYMENT_SECRET_OPTIONAL))) - assert(areSupported(ByteVector.fromLong(1L << BASIC_MULTI_PART_PAYMENT_MANDATORY))) - assert(areSupported(ByteVector.fromLong(1L << BASIC_MULTI_PART_PAYMENT_OPTIONAL))) + 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))) val testCases = Map( bin" 00000000000000001011" -> true, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 0847644fb4..eb8ff72f4a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair import java.util.concurrent.atomic.AtomicLong -import com.typesafe.config.{ConfigFactory, ConfigValue} +import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Block import fr.acinq.eclair.crypto.LocalKeyManager import org.scalatest.FunSuite @@ -28,20 +28,22 @@ import scala.util.Try class StartupSpec extends FunSuite { - test("check configuration") { + val defaultConf = ConfigFactory.parseResources("reference.conf").getConfig("eclair") + + def makeNodeParamsWithDefaults(conf: Config): NodeParams = { val blockCount = new AtomicLong(0) val keyManager = new LocalKeyManager(seed = randomBytes32, chainHash = Block.TestnetGenesisBlock.hash) - val conf = ConfigFactory.load().getConfig("eclair") - assert(Try(NodeParams.makeNodeParams(conf, keyManager, None, TestConstants.inMemoryDb(), blockCount, new TestConstants.TestFeeEstimator)).isSuccess) + val feeEstimator = new TestConstants.TestFeeEstimator + val db = TestConstants.inMemoryDb() + NodeParams.makeNodeParams(conf, keyManager, None, db, blockCount, feeEstimator) + } - val conf1 = conf.withFallback(ConfigFactory.parseMap(Map("max-feerate-mismatch" -> 42))) - intercept[RuntimeException] { - NodeParams.makeNodeParams(conf1, keyManager, None, TestConstants.inMemoryDb(), blockCount, new TestConstants.TestFeeEstimator) - } + test("check configuration") { + assert(Try(makeNodeParamsWithDefaults(ConfigFactory.load().getConfig("eclair"))).isSuccess) + assert(Try(makeNodeParamsWithDefaults(ConfigFactory.load().getConfig("eclair").withFallback(ConfigFactory.parseMap(Map("max-feerate-mismatch" -> 42))))).isFailure) } test("NodeParams should fail if the alias is illegal (over 32 bytes)") { - val threeBytesUTFChar = '\u20AC' // € val baseUkraineAlias = "BitcoinLightningNodeUkraine" @@ -55,14 +57,27 @@ class StartupSpec extends FunSuite { assert(goUkraineGo.getBytes.length === 33) // too long for the alias, should be truncated val illegalAliasConf = ConfigFactory.parseString(s"node-alias = $goUkraineGo") - val conf = illegalAliasConf.withFallback(ConfigFactory.parseResources("reference.conf").getConfig("eclair")) - val keyManager = new LocalKeyManager(seed = randomBytes32, chainHash = Block.TestnetGenesisBlock.hash) + val conf = illegalAliasConf.withFallback(defaultConf) - val blockCount = new AtomicLong(0) - - // try to create a NodeParams instance with a conf that contains an illegal alias - val nodeParamsAttempt = Try(NodeParams.makeNodeParams(conf, keyManager, None, TestConstants.inMemoryDb(), blockCount, new TestConstants.TestFeeEstimator)) + val nodeParamsAttempt = Try(makeNodeParamsWithDefaults(conf)) assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long")) } + test("NodeParams should fail with deprecated global-features or local-features") { + for (deprecated <- Seq("global-features", "local-features")) { + val illegalGlobalFeaturesConf = ConfigFactory.parseString(deprecated + " = \"0200\"") + val conf = illegalGlobalFeaturesConf.withFallback(defaultConf) + + val nodeParamsAttempt = Try(makeNodeParamsWithDefaults(conf)) + assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains(deprecated)) + } + } + + test("NodeParams should fail if features are inconsistent") { + val legalFeaturesConf = ConfigFactory.parseString("features = \"028a8a\"") + val illegalFeaturesConf = ConfigFactory.parseString("features = \"028000\"") // basic_mpp without var_onion_optin + assert(Try(makeNodeParamsWithDefaults(legalFeaturesConf.withFallback(defaultConf))).isSuccess) + assert(Try(makeNodeParamsWithDefaults(illegalFeaturesConf.withFallback(defaultConf))).isFailure) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index ca50021d65..1b3926db2a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress} -import scodec.bits.{ByteVector, HexStringSyntax} +import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -38,7 +38,6 @@ import scala.concurrent.duration._ object TestConstants { val defaultBlockHeight = 400000 - val globalFeatures = hex"0200" // variable_length_onion val fundingSatoshis = 1000000L sat val pushMsat = 200000000L msat val feeratePerKw = 10000L @@ -71,8 +70,7 @@ object TestConstants { alias = "alice", color = Color(1, 2, 3), publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, - globalFeatures = globalFeatures, - localFeatures = ByteVector.fromValidHex("088a"), + features = ByteVector.fromValidHex("0a8a"), overrideFeatures = Map.empty, syncWhitelist = Set.empty, dustLimit = 1100 sat, @@ -151,8 +149,7 @@ object TestConstants { alias = "bob", color = Color(4, 5, 6), publicAddresses = NodeAddress.fromParts("localhost", 9732).get :: Nil, - globalFeatures = globalFeatures, - localFeatures = ByteVector.empty, // no announcement + features = ByteVector.fromValidHex("0200"), // variable_length_onion, no announcement overrideFeatures = Map.empty, syncWhitelist = Set.empty, dustLimit = 1000 sat, @@ -220,4 +217,5 @@ object TestConstants { channelReserve = 20000 sat // Alice will need to keep that much satoshis as direct payment ) } + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala index 3f4511818c..1a55dc2fae 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala @@ -25,8 +25,8 @@ import scala.concurrent.duration._ import scala.util.Random /** - * A Fuzzy [[fr.acinq.eclair.Pipe]] which randomly disconnects/reconnects peers. - */ + * A Fuzzy [[fr.acinq.eclair.Pipe]] which randomly disconnects/reconnects peers. + */ class FuzzyPipe(fuzzy: Boolean) extends Actor with Stash with ActorLogging { import scala.concurrent.ExecutionContext.Implicits.global @@ -39,7 +39,7 @@ class FuzzyPipe(fuzzy: Boolean) extends Actor with Stash with ActorLogging { case _ => stash() } - def stayOrDisconnect(a: ActorRef, b: ActorRef, countdown: Int) = { + def stayOrDisconnect(a: ActorRef, b: ActorRef, countdown: Int): Unit = { if (!fuzzy) context become connected(a, b, countdown - 1) // fuzzy mode disabled, we never disconnect else if (countdown > 1) context become connected(a, b, countdown - 1) else { @@ -71,7 +71,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, ByteVector.empty) + val dummyInit = Init(ByteVector.empty) a ! INPUT_RECONNECTED(self, dummyInit, dummyInit) b ! INPUT_RECONNECTED(self, dummyInit, dummyInit) context become connected(a, b, Random.nextInt(40)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index d0863e89cf..d376a7db8a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -67,8 +67,8 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayerA)) val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayerB)) within(30 seconds) { - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) registerA ! alice registerB ! bob // no announcements diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala index d10a18a266..3d2decb287 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala @@ -12,10 +12,11 @@ import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, InputInfo import fr.acinq.eclair.wire.{ChannelReestablish, CommitSig, Error, Init, RevokeAndAck} import fr.acinq.eclair.{TestConstants, TestkitBaseClass, _} import org.scalatest.Outcome +import scodec.bits.ByteVector import scala.concurrent.duration._ -class RecoverySpec extends TestkitBaseClass with StateTestsHelperMethods { +class RecoverySpec extends TestkitBaseClass with StateTestsHelperMethods { type FixtureParam = SetupFixture @@ -33,9 +34,9 @@ class RecoverySpec extends TestkitBaseClass with StateTestsHelperMethods { } } - def aliceInit = Init(TestConstants.Alice.nodeParams.globalFeatures, TestConstants.Alice.nodeParams.localFeatures) + def aliceInit = Init(TestConstants.Alice.nodeParams.features) - def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) + def bobInit = Init(TestConstants.Bob.nodeParams.features) test("use funding pubkeys from publish commitment to spend our output") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index b067745111..e5fd050206 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -71,8 +71,8 @@ class ThroughputSpec extends FunSuite { val wallet = new TestWallet val alice = system.actorOf(Channel.props(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, blockchain, ???, relayerA, None), "a") val bob = system.actorOf(Channel.props(Bob.nodeParams, wallet, Alice.nodeParams.nodeId, blockchain, ???, relayerB, None), "b") - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 49b4589463..cd1446566e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -74,8 +74,8 @@ trait StateTestsHelperMethods extends TestKitBase with fixture.TestSuite with Pa val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) - val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) - val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) + val aliceInit = Init(aliceParams.features) + val bobInit = Init(bobParams.features) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelVersion) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 1b102e748d..8bfb218913 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -51,8 +51,8 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import setup._ val channelVersion = ChannelVersion.STANDARD val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) - val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) - val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) + val aliceInit = Init(aliceParams.features) + val bobInit = Init(bobParams.features) within(30 seconds) { alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 7e3f85896e..09a3378a8f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -40,8 +40,8 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper import setup._ val channelVersion = ChannelVersion.STANDARD val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) - val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) - val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) + val aliceInit = Init(aliceParams.features) + val bobInit = Init(bobParams.features) within(30 seconds) { alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 3537d27ef8..2ba60a438f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -44,8 +44,8 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State } val setup = init(wallet = noopWallet) import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) within(30 seconds) { alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 93be60649f..45d721ebf4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -45,8 +45,8 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel } else { (TestConstants.fundingSatoshis, TestConstants.pushMsat) } - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) within(30 seconds) { alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index fbe248ef21..98715d7a04 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -40,8 +40,8 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) within(30 seconds) { alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 5c54affbf6..3378785817 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -41,8 +41,8 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) within(30 seconds) { alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) @@ -84,7 +84,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH test("recv BITCOIN_FUNDING_DEPTHOK (bad funding pubkey script)") { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - val badOutputScript = fundingTx.txOut(0).copy(publicKeyScript = Script.write(multiSig2of2(randomKey.publicKey, randomKey.publicKey))) + val badOutputScript = fundingTx.txOut.head.copy(publicKeyScript = Script.write(multiSig2of2(randomKey.publicKey, randomKey.publicKey))) val badFundingTx = fundingTx.copy(txOut = Seq(badOutputScript)) alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42, badFundingTx) awaitCond(alice.stateName == CLOSED) @@ -93,7 +93,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH test("recv BITCOIN_FUNDING_DEPTHOK (bad funding amount)") { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - val badOutputAmount = fundingTx.txOut(0).copy(amount = 1234567.sat) + val badOutputAmount = fundingTx.txOut.head.copy(amount = 1234567.sat) val badFundingTx = fundingTx.copy(txOut = Seq(badOutputAmount)) alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42, badFundingTx) awaitCond(alice.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 9068f0dec1..b7406d1a5b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -40,8 +40,8 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) within(30 seconds) { alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 12ef573525..61a21530ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -34,6 +34,7 @@ import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} +import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -59,9 +60,9 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } - def aliceInit = Init(TestConstants.Alice.nodeParams.globalFeatures, TestConstants.Alice.nodeParams.localFeatures) + def aliceInit = Init(TestConstants.Alice.nodeParams.features) - def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) + def bobInit = Init(TestConstants.Bob.nodeParams.features) /** * This test checks the case where a disconnection occurs *right before* the counterparty receives a new sig @@ -504,8 +505,6 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateName == OFFLINE) val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] - val aliceCommitTx = aliceStateData.commitments.localCommit.publishableTxs.commitTx.tx - val localFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw val tooHighFeeratePerKw = ((alice.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch + 6) * localFeeratePerKw).toLong val highFeerate = FeeratesPerKw.single(tooHighFeeratePerKw) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index eb819c326c..861f2280a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -65,8 +65,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { if (unconfirmedFundingTx) { within(30 seconds) { - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] @@ -558,8 +558,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // then we manually replace alice's state with an older one alice.setState(OFFLINE, oldStateData) // then we reconnect them - val aliceInit = Init(TestConstants.Alice.nodeParams.globalFeatures, TestConstants.Alice.nodeParams.localFeatures) - val bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) + val aliceInit = Init(TestConstants.Alice.nodeParams.features) + val bobInit = Init(TestConstants.Bob.nodeParams.features) sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) // peers exchange channel_reestablish messages diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index 18ff9b2a38..7bd3cbc89f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -61,7 +61,7 @@ class SqliteAuditDbSpec extends FunSuite { val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true) val e11 = TrampolinePaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomKey.publicKey, Seq(randomBytes32), Seq(randomBytes32)) // TrampolinePaymentRelayed events are converted to ChannelPaymentRelayed events for now. We need to udpate the DB schema to fix this. - val e11bis = ChannelPaymentRelayed(42000 msat, 40000 msat, e11.paymentHash, e11.fromChannelIds.head, e11.toChannelIds.head) + val e11bis = ChannelPaymentRelayed(42000 msat, 40000 msat, e11.paymentHash, e11.fromChannelIds.head, e11.toChannelIds.head, e11.timestamp) db.add(e1) db.add(e2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 89d93fd0fb..da6c16cc39 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -147,14 +147,14 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("starting eclair nodes") { import collection.JavaConversions._ - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0)).withFallback(commonConfig)) // A's channels are private - instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true, "eclair.max-payment-attempts" -> 15)).withFallback(commonConfig)) - instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.features" -> "028a8a", "eclair.channel-flags" -> 0)).withFallback(commonConfig)) // A's channels are private + instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true, "eclair.max-payment-attempts" -> 15)).withFallback(commonConfig)) + instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig)) instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig)) instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085)).withFallback(commonConfig)) instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086)).withFallback(commonConfig)) - instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig)) + instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig)) instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.expiry-delta-blocks" -> 138, "eclair.server.port" -> 29738, "eclair.api.port" -> 28088)).withFallback(commonConfig)) instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.expiry-delta-blocks" -> 139, "eclair.server.port" -> 29739, "eclair.api.port" -> 28089)).withFallback(commonConfig)) instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig)) @@ -444,7 +444,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("send a multi-part payment B->D") { val sender = TestProbe() val amount = 1000000000L.msat - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "split the restaurant bill", allowMultiPart = true)) + sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "split the restaurant bill")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) @@ -477,7 +477,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // There is enough channel capacity to route this payment, but D doesn't have enough incoming capacity to receive it // (the link C->D has too much capacity on D's side). val amount = 2000000000L.msat - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "well that's an expensive restaurant bill", allowMultiPart = true)) + sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "well that's an expensive restaurant bill")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) @@ -504,7 +504,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sender = TestProbe() // This amount is greater than any channel capacity between D and C, so it should be split. val amount = 5100000000L.msat - sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), "lemme borrow some money", allowMultiPart = true)) + sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), "lemme borrow some money")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) @@ -532,7 +532,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sender = TestProbe() // This amount is greater than the current capacity between D and C. val amount = 10000000000L.msat - sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), "lemme borrow more money", allowMultiPart = true)) + sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), "lemme borrow more money")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) @@ -558,7 +558,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val start = Platform.currentTime val sender = TestProbe() val amount = 4000000000L.msat - sender.send(nodes("F3").paymentHandler, ReceivePayment(Some(amount), "like trampoline much?", allowMultiPart = true)) + sender.send(nodes("F3").paymentHandler, ReceivePayment(Some(amount), "like trampoline much?")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) assert(pr.features.allowTrampoline) @@ -591,7 +591,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val start = Platform.currentTime val sender = TestProbe() val amount = 2500000000L.msat - sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), "trampoline-MPP is so #reckless", allowMultiPart = true)) + sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), "trampoline-MPP is so #reckless")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) assert(pr.features.allowTrampoline) @@ -629,7 +629,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta))) val amount = 3000000000L.msat - sender.send(nodes("A").paymentHandler, ReceivePayment(Some(amount), "trampoline to non-trampoline is so #vintage", allowMultiPart = true, extraHops = routingHints)) + sender.send(nodes("A").paymentHandler, ReceivePayment(Some(amount), "trampoline to non-trampoline is so #vintage", extraHops = routingHints)) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) assert(!pr.features.allowTrampoline) @@ -662,7 +662,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sender = TestProbe() // We put most of the capacity C <-> D on D's side. - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(8000000000L msat), "plz send everything", allowMultiPart = true)) + sender.send(nodes("D").paymentHandler, ReceivePayment(Some(8000000000L msat), "plz send everything")) val pr1 = sender.expectMsgType[PaymentRequest](15 seconds) sender.send(nodes("C").paymentInitiator, SendPaymentRequest(8000000000L msat, pr1.paymentHash, nodes("D").nodeParams.nodeId, 3, paymentRequest = Some(pr1))) sender.expectMsgType[UUID](30 seconds) @@ -670,7 +670,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // Now we try to send more than C's outgoing capacity to D. val amount = 2000000000L.msat - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "I iz Satoshi", allowMultiPart = true)) + sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "I iz Satoshi")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) assert(pr.features.allowTrampoline) @@ -691,7 +691,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("send a trampoline payment A->D (temporary remote failure at trampoline)") { val sender = TestProbe() val amount = 2000000000L.msat // B can forward to C, but C doesn't have that much outgoing capacity to D - sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "I iz not Satoshi", allowMultiPart = true)) + sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "I iz not Satoshi")) val pr = sender.expectMsgType[PaymentRequest](15 seconds) assert(pr.features.allowMultiPart) assert(pr.features.allowTrampoline) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 851eb39888..f30b082ad7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -58,8 +58,8 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix val feeEstimator = new TestFeeEstimator val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayer)) val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Bob.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayer)) - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val aliceInit = Init(Alice.channelParams.features) + val bobInit = Init(Bob.channelParams.features) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(10000)) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 261b821620..7dc7bc0cb7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -22,7 +22,6 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.{ActorRef, PoisonPill} import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} @@ -32,7 +31,7 @@ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec, SendChannelQuery} -import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, TlvStream} +import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, LightningMessageCodecs, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, TlvStream} import org.scalatest.{Outcome, Tag} import scodec.bits.{ByteVector, _} @@ -76,7 +75,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { withFixture(test.toNoArgTest(FixtureParam(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer))) } - def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef, channels: Set[HasCommitments] = Set.empty, remoteInit: wire.Init = wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures), expectSync: Boolean = false): Unit = { + def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef, channels: Set[HasCommitments] = Set.empty, remoteInit: wire.Init = wire.Init(Bob.nodeParams.features), expectSync: Boolean = false): Unit = { // let's simulate a connection val probe = TestProbe() probe.send(peer, Peer.Init(None, channels)) @@ -130,7 +129,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { val Transition(_, INSTANTIATING, DISCONNECTED) = monitor.expectMsgType[Transition[_]] probe.send(peer, Peer.Connect(remoteNodeId, None)) - awaitCond(peer.stateData.address_opt == Some(fakeIPAddress.socketAddress)) + awaitCond(peer.stateData.address_opt === Some(fakeIPAddress.socketAddress)) } test("ignore connect to same address") { f => @@ -164,7 +163,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { // 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 mockAddress = NodeAddress.fromParts(mockServer.getInetAddress.getHostAddress, mockServer.getLocalPort).get val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) peer.underlyingActor.nodeParams.db.network.addNode(bobAnnouncement) @@ -206,7 +205,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(peer.stateData.asInstanceOf[DisconnectedData].nextReconnectionDelay === (initialReconnectDelay * 4)) } - test("disconnect if incompatible features") { f => + test("disconnect if incompatible local features") { f => import f._ val probe = TestProbe() probe.watch(transport.ref) @@ -214,8 +213,20 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, new InetSocketAddress("1.2.3.4", 42000), outgoing = true, None)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[wire.Init] - import scodec.bits._ - transport.send(peer, wire.Init(Bob.nodeParams.globalFeatures, bin"01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00".toByteVector)) + transport.send(peer, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value) + transport.expectMsgType[TransportHandler.ReadAck] + probe.expectTerminated(transport.ref) + } + + test("disconnect if incompatible global features") { f => + import f._ + val probe = TestProbe() + probe.watch(transport.ref) + probe.send(peer, Peer.Init(None, Set.empty)) + authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, new InetSocketAddress("1.2.3.4", 42000), outgoing = true, None)) + transport.expectMsgType[TransportHandler.Listener] + transport.expectMsgType[wire.Init] + transport.send(peer, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) } @@ -265,19 +276,19 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { test("sync if no whitelist is defined") { f => import f._ - val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + val remoteInit = wire.Init(bin"10000000".bytes) // bob supports channel range queries connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = true) } test("sync if whitelist contains peer", Tag("sync-whitelist-bob")) { f => import f._ - val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + val remoteInit = wire.Init(bin"0000001010000000".bytes) // bob supports channel range queries and variable length onion connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = true) } test("don't sync if whitelist doesn't contain peer", Tag("sync-whitelist-random")) { f => import f._ - val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + val remoteInit = wire.Init(bin"0000001010000000".bytes) // bob supports channel range queries connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = false) } @@ -346,7 +357,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap) - val timestamps = updates.map(_.timestamp).sorted.drop(10).take(20) + val timestamps = updates.map(_.timestamp).sorted.slice(10, 30) val filter = wire.GossipTimestampFilter(Alice.nodeParams.chainHash, timestamps.head, timestamps.last - timestamps.head) probe.send(peer, filter) probe.send(peer, rebroadcast) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 9423853e57..7fa445b2cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.relay.CommandBuffer import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, NodeParams, ShortChannelId, TestConstants, randomKey} import org.scalatest.{Outcome, fixture} +import scodec.bits.HexStringSyntax import scala.concurrent.duration._ @@ -41,16 +42,18 @@ import scala.concurrent.duration._ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike { - case class FixtureParam(nodeParams: NodeParams, defaultExpiry: CltvExpiry, handler: TestActorRef[PaymentHandler], commandBuffer: TestProbe, eventListener: TestProbe, sender: TestProbe) + 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)) + } override def withFixture(test: OneArgTest): Outcome = { within(30 seconds) { val nodeParams = Alice.nodeParams val commandBuffer = TestProbe() - val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, commandBuffer.ref)) val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) - withFixture(test.toNoArgTest(FixtureParam(nodeParams, CltvExpiryDelta(12).toCltvExpiry(nodeParams.currentBlockHeight), handler, commandBuffer, eventListener, TestProbe()))) + withFixture(test.toNoArgTest(FixtureParam(nodeParams, CltvExpiryDelta(12).toCltvExpiry(nodeParams.currentBlockHeight), commandBuffer, eventListener, TestProbe()))) } } @@ -60,7 +63,7 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun val amountMsat = 42000 msat { - sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) + sender.send(normalHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] val incoming = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) assert(incoming.isDefined) @@ -69,7 +72,7 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) + sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -82,13 +85,13 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun } { - sender.send(handler, ReceivePayment(Some(amountMsat), "another coffee with multi-part", allowMultiPart = true)) + sender.send(mppHandler, ReceivePayment(Some(amountMsat), "another coffee with multi-part")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) + sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -101,12 +104,12 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun } { - sender.send(handler, ReceivePayment(Some(amountMsat), "bad expiry")) + sender.send(normalHandler, ReceivePayment(Some(amountMsat), "bad expiry")) val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) + sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) @@ -120,17 +123,17 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun import f._ // negative amount should fail - sender.send(handler, ReceivePayment(Some(-50 msat), "1 coffee")) + sender.send(normalHandler, ReceivePayment(Some(-50 msat), "1 coffee")) val negativeError = sender.expectMsgType[Failure] assert(negativeError.cause.getMessage.contains("amount is not valid")) // amount = 0 should fail - sender.send(handler, ReceivePayment(Some(0 msat), "1 coffee")) + sender.send(normalHandler, ReceivePayment(Some(0 msat), "1 coffee")) val zeroError = sender.expectMsgType[Failure] assert(zeroError.cause.getMessage.contains("amount is not valid")) // success with 1 mBTC - sender.send(handler, ReceivePayment(Some(100000000 msat), "1 coffee")) + sender.send(normalHandler, ReceivePayment(Some(100000000 msat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.amount.contains(100000000 msat) && pr.nodeId.toString == nodeParams.nodeId.toString) } @@ -138,7 +141,7 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("Payment request generation should succeed when the amount is not set") { f => import f._ - sender.send(handler, ReceivePayment(None, "This is a donation PR")) + sender.send(normalHandler, ReceivePayment(None, "This is a donation PR")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.amount.isEmpty && pr.nodeId.toString == Alice.nodeParams.nodeId.toString) } @@ -146,10 +149,10 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("Payment request generation should handle custom expiries or use the default otherwise") { f => import f._ - sender.send(handler, ReceivePayment(Some(42000 msat), "1 coffee")) + sender.send(normalHandler, ReceivePayment(Some(42000 msat), "1 coffee")) assert(sender.expectMsgType[PaymentRequest].expiry === Some(Alice.nodeParams.paymentRequestExpiry.toSeconds)) - sender.send(handler, ReceivePayment(Some(42000 msat), "1 coffee with custom expiry", expirySeconds_opt = Some(60))) + sender.send(normalHandler, ReceivePayment(Some(42000 msat), "1 coffee with custom expiry", expirySeconds_opt = Some(60))) assert(sender.expectMsgType[PaymentRequest].expiry === Some(60)) } @@ -165,8 +168,8 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun } { - val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false), TestProbe().ref)) - sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee", allowMultiPart = true)) + val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = hex"028a8a"), TestProbe().ref)) + sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) assert(!pr.features.allowTrampoline) @@ -181,8 +184,8 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun } { - val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true), TestProbe().ref)) - sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee", allowMultiPart = true)) + val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = hex"028a8a"), TestProbe().ref)) + sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) assert(pr.features.allowTrampoline) @@ -200,23 +203,23 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun val route_x_z = extraHop_x_y :: extraHop_y_z :: Nil val route_x_t = extraHop_x_t :: Nil - sender.send(handler, ReceivePayment(Some(42000 msat), "1 coffee with additional routing info", extraHops = List(route_x_z, route_x_t))) + sender.send(normalHandler, ReceivePayment(Some(42000 msat), "1 coffee with additional routing info", extraHops = List(route_x_z, route_x_t))) assert(sender.expectMsgType[PaymentRequest].routingInfo === Seq(route_x_z, route_x_t)) - sender.send(handler, ReceivePayment(Some(42000 msat), "1 coffee without routing info")) + sender.send(normalHandler, ReceivePayment(Some(42000 msat), "1 coffee without routing info")) assert(sender.expectMsgType[PaymentRequest].routingInfo === Nil) } test("PaymentHandler should reject incoming payments if the payment request is expired") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "some desc", expirySeconds_opt = Some(0))) + sender.send(normalHandler, ReceivePayment(Some(1000 msat), "some desc", expirySeconds_opt = Some(0))) val pr = sender.expectMsgType[PaymentRequest] assert(!pr.features.allowMultiPart) assert(pr.isExpired) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) + sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry))) commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]] val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired) @@ -225,13 +228,13 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("PaymentHandler should reject incoming multi-part payment if the payment request is expired") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "multi-part expired", expirySeconds_opt = Some(0), allowMultiPart = true)) + sender.send(mppHandler, ReceivePayment(Some(1000 msat), "multi-part expired", expirySeconds_opt = Some(0))) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) assert(pr.isExpired) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) + sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) @@ -241,12 +244,12 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("PaymentHandler should reject incoming multi-part payment if the payment request does not allow it") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "no multi-part support")) + sender.send(normalHandler, ReceivePayment(Some(1000 msat), "no multi-part support")) val pr = sender.expectMsgType[PaymentRequest] assert(!pr.features.allowMultiPart) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) + sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) @@ -255,12 +258,12 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("PaymentHandler should reject incoming multi-part payment with an invalid expiry") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "multi-part invalid expiry", allowMultiPart = true)) + sender.send(mppHandler, ReceivePayment(Some(1000 msat), "multi-part invalid expiry")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, CltvExpiryDelta(1).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) + sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) @@ -269,12 +272,12 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("PaymentHandler should reject incoming multi-part payment with an unknown payment hash") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "multi-part unknown payment hash", allowMultiPart = true)) + sender.send(mppHandler, ReceivePayment(Some(1000 msat), "multi-part unknown payment hash")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) + sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) @@ -283,12 +286,12 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("PaymentHandler should reject incoming multi-part payment with a total amount too low") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "multi-part total amount too low", allowMultiPart = true)) + sender.send(mppHandler, ReceivePayment(Some(1000 msat), "multi-part total amount too low")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get))) + sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) @@ -297,12 +300,12 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("PaymentHandler should reject incoming multi-part payment with a total amount too high") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "multi-part total amount too low", allowMultiPart = true)) + sender.send(mppHandler, ReceivePayment(Some(1000 msat), "multi-part total amount too low")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get))) + sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) @@ -311,30 +314,30 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("PaymentHandler should reject incoming multi-part payment with an invalid payment secret") { f => import f._ - sender.send(handler, ReceivePayment(Some(1000 msat), "multi-part invalid payment secret", allowMultiPart = true)) + sender.send(mppHandler, ReceivePayment(Some(1000 msat), "multi-part invalid payment secret")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) // Invalid payment secret. val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse))) + sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse))) val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) } test("PaymentHandler should handle multi-part payment timeout") { f => - val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 200 millis) + val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 200 millis, features = hex"028a8a") val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref)) // Partial payment missing additional parts. - f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 slow coffee", allowMultiPart = true)) + f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 slow coffee")) val pr1 = f.sender.expectMsgType[PaymentRequest] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) f.sender.send(handler, IncomingPacket.FinalPacket(add1, Onion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get))) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. - f.sender.send(handler, ReceivePayment(Some(1500 msat), "1 slow latte", allowMultiPart = true)) + f.sender.send(handler, ReceivePayment(Some(1500 msat), "1 slow latte")) val pr2 = f.sender.expectMsgType[PaymentRequest] val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) f.sender.send(handler, IncomingPacket.FinalPacket(add2, Onion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get))) @@ -362,10 +365,10 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun } test("PaymentHandler should handle multi-part payment success") { f => - val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis) + val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = hex"028a8a") val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref)) - f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 fast coffee", allowMultiPart = true)) + f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 fast coffee")) val pr = f.sender.expectMsgType[PaymentRequest] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) @@ -408,10 +411,10 @@ class MultiPartHandlerSpec extends TestKit(ActorSystem("test")) with fixture.Fun } test("PaymentHandler should handle multi-part payment timeout then success") { f => - val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 250 millis) + val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 250 millis, features = hex"028a8a") val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref)) - f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 coffee, no sugar", allowMultiPart = true)) + f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 coffee, no sugar")) val pr = f.sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 7cf070e85a..a4c6d7d9b1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -567,8 +567,8 @@ object MultiPartPaymentLifecycleSpec { import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain // We are only interested in availableBalanceForSend so we can put dummy values in most places. - val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64(50000000), 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, ByteVector.empty, ByteVector.empty) - val remoteParams = RemoteParams(randomKey.publicKey, 0 sat, UInt64(5000000), 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty, ByteVector.empty) + val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64(50000000), 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, ByteVector.empty) + val remoteParams = RemoteParams(randomKey.publicKey, 0 sat, UInt64(5000000), 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, canSend.truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey) Commitments( ChannelVersion.STANDARD, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala index bb61570a5c..08a6cd21df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala @@ -260,7 +260,8 @@ class NodeRelayerSpec extends TestkitBaseClass { // 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 * 3), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(Features(BASIC_MULTI_PART_PAYMENT_OPTIONAL, PAYMENT_SECRET_MANDATORY))) + val features = Features(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 )))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index bcd7d842cf..d33f50e9ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -103,7 +103,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("forward multi-part payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(BASIC_MULTI_PART_PAYMENT_OPTIONAL, PAYMENT_SECRET_OPTIONAL))) + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(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] @@ -113,7 +113,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("forward multi-part payment with pre-defined route") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(BASIC_MULTI_PART_PAYMENT_OPTIONAL, PAYMENT_SECRET_OPTIONAL))) + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) val req = SendPaymentRequest(finalAmount / 2, paymentHash, c, 1, paymentRequest = Some(pr), predefinedRoute = Seq(a, b, c)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] @@ -128,7 +128,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun test("forward trampoline payment") { f => import f._ - val features = Features(PAYMENT_SECRET_OPTIONAL, BASIC_MULTI_PART_PAYMENT_OPTIONAL, TRAMPOLINE_PAYMENT_OPTIONAL) + val features = Features(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 @@ -195,14 +195,14 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun assert(trampolinePayload.outgoingCltv.toLong === currentBlockCount + 9 + 1) assert(trampolinePayload.outgoingNodeId === c) assert(trampolinePayload.paymentSecret === pr.paymentSecret) - assert(trampolinePayload.invoiceFeatures === Some(hex"8000")) // PAYMENT_SECRET_OPTIONAL + assert(trampolinePayload.invoiceFeatures === Some(hex"8200")) // var_onion_optin, payment_secret } test("reject trampoline to legacy payment for 0-value invoice") { f => 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(PAYMENT_SECRET_OPTIONAL, BASIC_MULTI_PART_PAYMENT_OPTIONAL) + val features = Features(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, trampolineFees, pr, b, CltvExpiryDelta(9), CltvExpiryDelta(12)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index e26afe65aa..163b4778a5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -68,7 +68,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { assert(onion.packet.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val features = if (legacy) ByteVector.empty else TestConstants.globalFeatures + val features = if (legacy) ByteVector.empty else variableLengthOnionFeature testPeelOnion(onion.packet, features) } @@ -168,7 +168,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { assert(payload_b === RelayLegacyPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc)) val add_c = UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c) - val Right(NodeRelayPacket(add_c2, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey, TestConstants.globalFeatures) + val Right(NodeRelayPacket(add_c2, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey, variableLengthOnionFeature) assert(add_c2 === add_c) assert(outer_c.amount === amount_bc) assert(outer_c.totalAmount === amount_bc) @@ -185,7 +185,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { assert(amount_d === amount_cd) assert(expiry_d === expiry_cd) val add_d = UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet) - val Right(NodeRelayPacket(add_d2, outer_d, inner_d, packet_e)) = decrypt(add_d, priv_d.privateKey, TestConstants.globalFeatures) + val Right(NodeRelayPacket(add_d2, outer_d, inner_d, packet_e)) = decrypt(add_d, priv_d.privateKey, variableLengthOnionFeature) assert(add_d2 === add_d) assert(outer_d.amount === amount_cd) assert(outer_d.totalAmount === amount_cd) @@ -202,7 +202,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { assert(amount_e === amount_de) assert(expiry_e === expiry_de) val add_e = UpdateAddHtlc(randomBytes32, 4, amount_e, paymentHash, expiry_e, onion_e.packet) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, TestConstants.globalFeatures) + val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, variableLengthOnionFeature) assert(add_e2 === add_e) assert(payload_e === FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3)))) } @@ -214,7 +214,7 @@ class PaymentPacketSpec extends FunSuite 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(PAYMENT_SECRET_OPTIONAL, BASIC_MULTI_PART_PAYMENT_OPTIONAL) + val invoiceFeatures = Features(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) @@ -228,7 +228,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, ByteVector.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, TestConstants.globalFeatures) + val Right(NodeRelayPacket(_, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey, variableLengthOnionFeature) assert(outer_c.amount === amount_bc) assert(outer_c.totalAmount === amount_bc) assert(outer_c.expiry === expiry_bc) @@ -245,7 +245,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { assert(amount_d === amount_cd) assert(expiry_d === expiry_cd) val add_d = UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet) - val Right(NodeRelayPacket(_, outer_d, inner_d, _)) = decrypt(add_d, priv_d.privateKey, TestConstants.globalFeatures) + val Right(NodeRelayPacket(_, outer_d, inner_d, _)) = decrypt(add_d, priv_d.privateKey, variableLengthOnionFeature) assert(outer_d.amount === amount_cd) assert(outer_d.totalAmount === amount_cd) assert(outer_d.expiry === expiry_cd) @@ -255,7 +255,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { assert(inner_d.outgoingNodeId === e) assert(inner_d.totalAmount === finalAmount) assert(inner_d.paymentSecret === invoice.paymentSecret) - assert(inner_d.invoiceFeatures === Some(hex"028000")) // PAYMENT_SECRET_OPTIONAL, BASIC_MULTI_PART_PAYMENT_OPTIONAL + assert(inner_d.invoiceFeatures === Some(hex"028200")) // var_onion_optin, payment_secret, basic_mpp assert(inner_d.invoiceRoutingInfo === Some(routingHints)) } @@ -270,7 +270,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { test("fail to decrypt when the onion is invalid") { val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry)) val add = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse)) - val Left(failure) = decrypt(add, priv_b.privateKey, TestConstants.globalFeatures) + val Left(failure) = decrypt(add, priv_b.privateKey, variableLengthOnionFeature) assert(failure.isInstanceOf[InvalidOnionHmac]) } @@ -280,14 +280,14 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { 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 add_c = UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c) - val Left(failure) = decrypt(add_c, priv_c.privateKey, TestConstants.globalFeatures) + val Left(failure) = decrypt(add_c, priv_c.privateKey, variableLengthOnionFeature) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt when payment hash doesn't match associated data") { val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash.reverse, hops, FinalLegacyPayload(finalAmount, finalExpiry)) val add = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet) - val Left(failure) = decrypt(add, priv_b.privateKey, TestConstants.globalFeatures) + val Left(failure) = decrypt(add, priv_b.privateKey, variableLengthOnionFeature) assert(failure.isInstanceOf[InvalidOnionHmac]) } @@ -316,14 +316,14 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { 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(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, TestConstants.globalFeatures) + 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)) - val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey, TestConstants.globalFeatures) + val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey, variableLengthOnionFeature) // d forwards an invalid amount to e (the outer total amount doesn't match the inner amount). val invalidTotalAmount = amount_de + 100.msat val (amount_e, expiry_e, onion_e) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, Onion.createTrampolinePayload(amount_de, invalidTotalAmount, expiry_de, randomBytes32, packet_e)) - val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey, TestConstants.globalFeatures) + val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey, variableLengthOnionFeature) assert(failure === FinalIncorrectHtlcAmount(invalidTotalAmount)) } @@ -331,14 +331,14 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { 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(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, TestConstants.globalFeatures) + 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)) - val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey, TestConstants.globalFeatures) + val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey, variableLengthOnionFeature) // d forwards an invalid expiry to e (the outer expiry doesn't match the inner expiry). val invalidExpiry = expiry_de - CltvExpiryDelta(12) val (amount_e, expiry_e, onion_e) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, Onion.createTrampolinePayload(amount_de, amount_de, invalidExpiry, randomBytes32, packet_e)) - val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey, TestConstants.globalFeatures) + val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey, variableLengthOnionFeature) assert(failure === FinalIncorrectCltvExpiry(invalidExpiry)) } @@ -346,13 +346,13 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { 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(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, TestConstants.globalFeatures) + 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)) - val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey, TestConstants.globalFeatures) + val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32, 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey, variableLengthOnionFeature) // d forwards the trampoline payment to e. val (amount_e, expiry_e, onion_e) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, Onion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32, packet_e)) - val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey, TestConstants.globalFeatures) + val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey, variableLengthOnionFeature) assert(failure === InvalidOnionPayload(UInt64(8), 0)) } @@ -361,7 +361,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { 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) // 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, TestConstants.globalFeatures) + 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)) } @@ -370,7 +370,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { 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) // 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, TestConstants.globalFeatures) + 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))) } @@ -378,6 +378,8 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { object PaymentPacketSpec { + val variableLengthOnionFeature = ByteVector.fromLong(1L << 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 = { require(nodes.size == payloads.size) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index 00eb27be32..a81009bb36 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -20,7 +20,7 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Protocol} -import fr.acinq.eclair.Features._ +import fr.acinq.eclair.Features.{PaymentSecret, _} import fr.acinq.eclair.payment.PaymentRequest._ import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, ToMilliSatoshiConversion} import org.scalatest.FunSuite @@ -217,8 +217,8 @@ class PaymentRequestSpec extends FunSuite { assert(PaymentRequest.write(pr.sign(priv)) == ref) } - test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 15 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { - val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqqq4u9s93jtgysm3mrwll70zr697y3mf902hvxwej0v7c62rsltw83ng0pu8w3j230sluc5gxkdmm9dvpy9y6ggtjd2w544mzdrcs42t7sqdkcy8h" + test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { + val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu" val pr = PaymentRequest.read(ref) assert(pr.prefix === "lnbc") assert(pr.amount === Some(2500000000L msat)) @@ -228,7 +228,7 @@ class PaymentRequestSpec extends FunSuite { assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description === Left("coffee beans")) assert(pr.fallbackAddress().isEmpty) - assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000") + assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000") assert(!pr.features.allowMultiPart) assert(!pr.features.requirePaymentSecret) assert(!pr.features.allowTrampoline) @@ -236,8 +236,8 @@ class PaymentRequestSpec extends FunSuite { assert(PaymentRequest.write(pr.sign(priv)) === ref) } - test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 15, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { - val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqqqqu7fz6pjqczdm3jp3qps7xntj2w2mm70e0ckhw3c5xk9p36pvk3sewn7ncaex6uzfq0vtqzy28se6pcwn790vxex7xystzumhg55p6qq9wq7td" + test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { + val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" val pr = PaymentRequest.read(ref) assert(pr.prefix === "lnbc") assert(pr.amount === Some(2500000000L msat)) @@ -247,7 +247,7 @@ class PaymentRequestSpec extends FunSuite { assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description === Left("coffee beans")) assert(pr.fallbackAddress().isEmpty) - assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000") + assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000") assert(!pr.features.allowMultiPart) assert(!pr.features.requirePaymentSecret) assert(!pr.features.allowTrampoline) @@ -312,11 +312,11 @@ class PaymentRequestSpec extends FunSuite { val featureBits = Map( Features(bin" 00000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), - Features(bin" 00011000000000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), - Features(bin" 00101000000000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), - Features(bin" 00010100000000000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - Features(bin" 00011000000000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), - Features(bin" 00101000000000000000") -> Result(allowMultiPart = true, 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 = false), Features(bin" 0000010000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), Features(bin" 0000011000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), @@ -357,7 +357,7 @@ class PaymentRequestSpec extends FunSuite { 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(PAYMENT_SECRET_OPTIONAL)) + assert(pr.features === Features(PaymentSecret.optional, VariableLengthOnion.optional)) assert(!pr.features.requirePaymentSecret) val pr1 = PaymentRequest.read(PaymentRequest.write(pr)) @@ -369,12 +369,12 @@ class PaymentRequestSpec extends FunSuite { // An invoice that sets the payment secret feature bit must provide a payment secret. assertThrows[IllegalArgumentException]( - PaymentRequest.read("lntb15u1pwahg4kpp56hhnss8tshz2qz539a0u69yjlcq4vpm7776tuqqv53mqn8eeslpsdq4xysyymr0vd4kzcmrd9hx7cqp29qy9qqq6t3lep4wkqeuj4y58fwxj6lqykzqdaa7a7cak4elywptgft0mcfnl0k243870ek8dwnnqww67wrak5kxpfw428rgu58z66er76sh5zsqw0zvns") + PaymentRequest.read("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7") ) // A multi-part invoice must use a payment secret. assertThrows[IllegalArgumentException]( - PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "MPP without secrets", features = Some(Features(BASIC_MULTI_PART_PAYMENT_OPTIONAL))) + PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "MPP without secrets", features = Some(Features(BasicMultiPartPayment.optional, VariableLengthOnion.optional))) ) } @@ -382,11 +382,11 @@ class PaymentRequestSpec extends FunSuite { 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(TRAMPOLINE_PAYMENT_OPTIONAL))) + val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(Features(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(TRAMPOLINE_PAYMENT_MANDATORY, BASIC_MULTI_PART_PAYMENT_OPTIONAL, PAYMENT_SECRET_OPTIONAL))) + 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))) assert(pr2.features.allowMultiPart) assert(pr2.features.allowTrampoline) @@ -453,8 +453,8 @@ class PaymentRequestSpec extends FunSuite { "lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx", "lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9", "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9", - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqqq4u9s93jtgysm3mrwll70zr697y3mf902hvxwej0v7c62rsltw83ng0pu8w3j230sluc5gxkdmm9dvpy9y6ggtjd2w544mzdrcs42t7sqdkcy8h", - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqqqqu7fz6pjqczdm3jp3qps7xntj2w2mm70e0ckhw3c5xk9p36pvk3sewn7ncaex6uzfq0vtqzy28se6pcwn790vxex7xystzumhg55p6qq9wq7td" + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu", + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" ) for (req <- requests) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 4f628ede35..661e4e99d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -48,8 +48,8 @@ class AnnouncementsSpec extends FunSuite { } test("create valid signed node announcement") { - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.globalFeatures) - assert(Features.hasFeature(ann.features, Features.VARIABLE_LENGTH_ONION_OPTIONAL)) + 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, Some(FeatureSupport.Optional))) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index e34f735ba0..caed96b65d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -95,11 +95,15 @@ class ChannelCodecsSpec extends FunSuite { maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), isFunder = Random.nextBoolean(), - globalFeatures = randomBytes(256), - localFeatures = randomBytes(256)) + features = randomBytes(256)) val encoded = localParamsCodec.encode(o).require val decoded = localParamsCodec.decode(encoded).require assert(o === decoded.value) + + // Backwards-compatibility: decode localparams with global features. + val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" + val withGlobalFeaturesDecoded = localParamsCodec.decode(withGlobalFeatures.bits).require.value + assert(withGlobalFeaturesDecoded.features === hex"0a8a") } test("encode/decode remoteparams") { @@ -116,11 +120,15 @@ class ChannelCodecsSpec extends FunSuite { paymentBasepoint = randomKey.publicKey, delayedPaymentBasepoint = randomKey.publicKey, htlcBasepoint = randomKey.publicKey, - globalFeatures = randomBytes(256), - localFeatures = randomBytes(256)) + features = randomBytes(256)) val encoded = remoteParamsCodec.encode(o).require val decoded = remoteParamsCodec.decodeValue(encoded).require assert(o === decoded) + + // Backwards-compatibility: decode remoteparams with global features. + val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a" + val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value + assert(withGlobalFeaturesDecoded.features === hex"028a") } test("encode/decode direction") { @@ -309,8 +317,8 @@ class ChannelCodecsSpec extends FunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":204739729,"toRemoteMsat":16572475271},"publishableTxs":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"0200000000010107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce1112040047304402204d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a8022065845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d01483045022100f968fb38342997065f66c38731d4ce592a85e6952175a8511f4d58bf93d308b8022039ee395d24a5a71226bb18c0c44bb9e3c066799be3f5d096e9f8ba7a88184bf101475221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae7af8a620"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":16572475271,"toRemoteMsat":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null,"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c"},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":"","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3"},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000}}""", - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":1343316620,"toRemoteMsat":13656683380},"publishableTxs":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"020000000001016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f490400483045022100bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af6231702203b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f01483045022100e9e60db46ea3709d8bff62ab7b94f71c0441177b791a9e664f574aaf7e4f9a2e02205de95919925e8d3b52c85a9a82c578a8bf16695bc2b6fadf330115445610d603014752210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52aedf013320"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":13656683380,"toRemoteMsat":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null,"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611"},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000}}""" + hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","features":"8a"},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","features":"81"},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":204739729,"toRemoteMsat":16572475271},"publishableTxs":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"0200000000010107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce1112040047304402204d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a8022065845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d01483045022100f968fb38342997065f66c38731d4ce592a85e6952175a8511f4d58bf93d308b8022039ee395d24a5a71226bb18c0c44bb9e3c066799be3f5d096e9f8ba7a88184bf101475221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae7af8a620"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":16572475271,"toRemoteMsat":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null,"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c"},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":"","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3"},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000}}""", + hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","features":"8a"},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","features":"81"},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":1343316620,"toRemoteMsat":13656683380},"publishableTxs":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"020000000001016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f490400483045022100bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af6231702203b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f01483045022100e9e60db46ea3709d8bff62ab7b94f71c0441177b791a9e664f574aaf7e4f9a2e02205de95919925e8d3b52c85a9a82c578a8bf16695bc2b6fadf330115445610d603014752210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52aedf013320"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":13656683380,"toRemoteMsat":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null,"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611"},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000}}""" ) refs.foreach { case (oldbin, refjson) => @@ -362,8 +370,7 @@ object ChannelCodecsSpec { maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, isFunder = true, - globalFeatures = hex"dead", - localFeatures = hex"beef") + features = hex"deadbeef") val remoteParams = RemoteParams( nodeId = randomKey.publicKey, @@ -378,8 +385,7 @@ object ChannelCodecsSpec { paymentBasepoint = PrivateKey(ByteVector.fill(32)(3)).publicKey, delayedPaymentBasepoint = PrivateKey(ByteVector.fill(32)(4)).publicKey, htlcBasepoint = PrivateKey(ByteVector.fill(32)(6)).publicKey, - globalFeatures = hex"dead", - localFeatures = hex"beef") + features = hex"deadbeef") val paymentPreimages = Seq( ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index 03f01c3d59..221b33965f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ -import ReplyChannelRangeTlv._ +import fr.acinq.eclair.wire.ReplyChannelRangeTlv._ import org.scalatest.FunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -43,6 +43,25 @@ class LightningMessageCodecsSpec extends FunSuite { def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey + test("encode/decode init message") { + val testCases = Seq( + (hex"0000 0000", hex"", hex"0000 0000"), // no features + (hex"0000 0002088a", hex"088a", hex"0000 0002088a"), // no global features + (hex"00020200 0000", hex"0200", hex"0000 00020200"), // no local features + (hex"00020200 0002088a", hex"0a8a", hex"0000 00020a8a"), // local and global - no conflict - same size + (hex"00020200 0003020002", hex"020202", hex"0000 0003020202"), // local and global - no conflict - different sizes + (hex"00020a02 0002088a", hex"0a8a", hex"0000 00020a8a"), // local and global - conflict - same size + (hex"00022200 000302aaa2", hex"02aaa2", hex"0000 000302aaa2") // local and global - conflict - different sizes + ) + + for ((bin, features, encoded) <- testCases) { + val init = initCodec.decode(bin.bits).require.value + assert(init.features === features) + assert(initCodec.encode(init).require.bytes === encoded) + assert(initCodec.decode(encoded.bits).require.value === init) + } + } + test("encode/decode live node_announcements") { val ann = hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce2690001025acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607" val bin = ann.bits @@ -77,7 +96,7 @@ class LightningMessageCodecsSpec extends FunSuite { val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, - TlvStream(QueryChannelRangeTlv.QueryFlags((QueryChannelRangeTlv.QueryFlags.WANT_ALL)) :: Nil, unknownTlv :: Nil)) + TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL) :: Nil, unknownTlv :: Nil)) val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream( @@ -127,7 +146,7 @@ class LightningMessageCodecsSpec extends FunSuite { val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 35000, 100, - TlvStream(QueryChannelRangeTlv.QueryFlags((QueryChannelRangeTlv.QueryFlags.WANT_ALL)))) + TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL))) val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 756230, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None, None) val reply_channel_range_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 1600, 110, 1, @@ -145,8 +164,6 @@ class LightningMessageCodecsSpec extends FunSuite { val query_short_channel_id_flags = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) val query_short_channel_id_flags_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(14200), ShortChannelId(46645), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) - - val refs = Map( query_channel_range -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000186a0000005dc", query_channel_range_timestamps_checksums -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala index c9be816d13..61b6939b5f 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -248,8 +248,8 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("createinvoice") { - formFields("description".as[String], amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](sha256HashUnmarshaller).?, "allowMultiPart".as[Boolean].?) { (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt, allowMultiPart_opt) => - complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt, allowMultiPart_opt.getOrElse(false))) + formFields("description".as[String], amountMsatFormParam.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[ByteVector32](sha256HashUnmarshaller).?) { (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => + complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt)) } } ~ path("getinvoice") { @@ -290,7 +290,7 @@ trait Service extends ExtraDirectives with Logging { path("usablebalances") { complete(eclairApi.usableBalances()) } ~ - path("getnewaddress"){ + path("getnewaddress") { complete(eclairApi.newAddress()) } } ~ get {