From a6da1db46f2bf2050c66d9f81a8a7b5e30c1ac8f Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 12 Dec 2019 16:34:25 +0100 Subject: [PATCH 1/8] Flat Features Implement https://github.com/lightningnetwork/lightning-rfc/pull/666 We keep the global/local split in Commitments to avoid backwards incompatibility in the codec. --- eclair-core/src/main/resources/reference.conf | 6 +-- .../scala/fr/acinq/eclair/NodeParams.scala | 23 +++++----- .../main/scala/fr/acinq/eclair/io/Peer.scala | 21 +++++---- .../relay/PostRestartHtlcCleaner.scala | 2 +- .../acinq/eclair/payment/relay/Relayer.scala | 2 +- .../scala/fr/acinq/eclair/router/Router.scala | 4 +- .../eclair/wire/LightningMessageCodecs.scala | 10 ++--- .../eclair/wire/LightningMessageTypes.scala | 19 ++++++-- .../scala/fr/acinq/eclair/StartupSpec.scala | 18 +++++++- .../scala/fr/acinq/eclair/TestConstants.scala | 10 ++--- .../acinq/eclair/channel/RecoverySpec.scala | 7 +-- .../channel/states/e/OfflineStateSpec.scala | 7 ++- .../channel/states/h/ClosingStateSpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 33 +++++++++----- .../eclair/payment/PaymentPacketSpec.scala | 44 ++++++++++--------- .../eclair/router/AnnouncementsSpec.scala | 2 +- .../wire/LightningMessageCodecsSpec.scala | 26 ++++++++--- 17 files changed, 145 insertions(+), 93 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8a3831eae6..27d42f0b04 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -37,13 +37,11 @@ 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 = "" + # features = "", # } ] sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers 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..3c46a53f23 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, @@ -130,11 +129,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 +173,10 @@ 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 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 +221,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 = ByteVector.fromValidHex(config.getString("features")), overrideFeatures = overrideFeatures, syncWhitelist = syncWhitelist, dustLimit = dustLimitSatoshis, 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..7193de3811 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,8 +101,8 @@ 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(ByteVector.empty, f) + case None => wire.Init(ByteVector.empty, nodeParams.features) } log.info(s"using globalFeatures=${localInit.globalFeatures.toBin} and localFeatures=${localInit.localFeatures.toBin}") transport ! localInit @@ -137,14 +136,14 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A log.info(s"peer is using globalFeatures=${remoteInit.globalFeatures.toBin} and localFeatures=${remoteInit.localFeatures.toBin}") - if (Features.areSupported(remoteInit.localFeatures)) { + if (Features.areSupported(remoteInit.features)) { d.origin_opt.foreach(origin => origin ! "connected") import Features._ - def hasLocalFeature(bit: Int) = Features.hasFeature(d.localInit.localFeatures, bit) + def hasLocalFeature(bit: Int) = Features.hasFeature(d.localInit.features, bit) - def hasRemoteFeature(bit: Int) = Features.hasFeature(remoteInit.localFeatures, bit) + def hasRemoteFeature(bit: Int) = Features.hasFeature(remoteInit.features, 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)) @@ -548,7 +547,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 +684,8 @@ object Peer { maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, defaultFinalScriptPubKey = defaultFinalScriptPubKey, isFunder = isFunder, - globalFeatures = nodeParams.globalFeatures, - localFeatures = nodeParams.localFeatures) + globalFeatures = ByteVector.empty, + localFeatures = nodeParams.features) } /** 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/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 408c2b8a69..3680975cab 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,13 +16,13 @@ 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.codecs._ +import scodec.{Attempt, Codec} /** * Created by PM on 15/11/2016. @@ -239,10 +239,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 +250,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..43482b8ee2 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,22 @@ 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 +/** For historical reasons, features are divided into two feature bitmasks. We only use the second one in our messages, but we allow receiving in both. */ +case class Init(globalFeatures: ByteVector, localFeatures: ByteVector) extends SetupMessage { + val features = combineFeatures(globalFeatures, localFeatures) + + private def combineFeatures(f1: ByteVector, f2: ByteVector): ByteVector = { + if (f1.length == f2.length) { + f1 | f2 + } else if (f1.length > f2.length) { + f1 | f2.padLeft(f1.length) + } else { + f1.padLeft(f2.length) | f2 + } + } +} -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/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 0847644fb4..436d2d02f4 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.ConfigFactory import fr.acinq.bitcoin.Block import fr.acinq.eclair.crypto.LocalKeyManager import org.scalatest.FunSuite @@ -41,7 +41,6 @@ class StartupSpec extends FunSuite { } test("NodeParams should fail if the alias is illegal (over 32 bytes)") { - val threeBytesUTFChar = '\u20AC' // € val baseUkraineAlias = "BitcoinLightningNodeUkraine" @@ -65,4 +64,19 @@ class StartupSpec extends FunSuite { assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long")) } + test("NodeParams should fail with deprecated global-features or local-features") { + val blockCount = new AtomicLong(0) + val keyManager = new LocalKeyManager(seed = randomBytes32, chainHash = Block.TestnetGenesisBlock.hash) + val feeEstimator = new TestConstants.TestFeeEstimator + val db = TestConstants.inMemoryDb() + + for (deprecated <- Seq("global-features", "local-features")) { + val illegalGlobalFeaturesConf = ConfigFactory.parseString(deprecated + " = \"0200\"") + val conf = illegalGlobalFeaturesConf.withFallback(ConfigFactory.parseResources("reference.conf").getConfig("eclair")) + + val nodeParamsAttempt = Try(NodeParams.makeNodeParams(conf, keyManager, None, db, blockCount, feeEstimator)) + assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains(deprecated)) + } + } + } 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/RecoverySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala index d10a18a266..afdc956a93 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(ByteVector.empty, TestConstants.Alice.nodeParams.features) - def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) + def bobInit = Init(ByteVector.empty, 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/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 12ef573525..cfe3519d82 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(ByteVector.empty, TestConstants.Alice.nodeParams.features) - def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) + def bobInit = Init(ByteVector.empty, 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..d4519fa26b 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 @@ -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(ByteVector.empty, TestConstants.Alice.nodeParams.features) + val bobInit = Init(ByteVector.empty, 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/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 261b821620..466c509c28 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} @@ -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(ByteVector.empty, 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, wire.Init(ByteVector.empty, bin"01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00".bytes)) + 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, wire.Init(bin"01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00".bytes, ByteVector.empty)) 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(ByteVector.empty, 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(hex"0200", bin"10000000".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(ByteVector.empty, bin"10000000".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/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index e26afe65aa..745ea9f65a 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)))) } @@ -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) @@ -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,10 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { object PaymentPacketSpec { + import fr.acinq.eclair.Features.VARIABLE_LENGTH_ONION_OPTIONAL + + val variableLengthOnionFeature = ByteVector.fromLong(1L << VARIABLE_LENGTH_ONION_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/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 4f628ede35..122e8a039b 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,7 +48,7 @@ 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) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.features) assert(Features.hasFeature(ann.features, Features.VARIABLE_LENGTH_ONION_OPTIONAL)) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) 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..caff51decd 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,24 @@ 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""), // no features + (hex"0000 0002088a", hex"088a"), // no global features + (hex"00020200 0000", hex"0200"), // no local features + (hex"00020200 0002088a", hex"0a8a"), // local and global - no conflict - same size + (hex"00020200 0003020002", hex"020202"), // local and global - no conflict - different sizes + (hex"00020a02 0002088a", hex"0a8a"), // local and global - conflict - same size + (hex"00022200 000302aaa2", hex"02aaa2") // local and global - conflict - different sizes + ) + + for ((bin, features) <- testCases) { + val init = initCodec.decode(bin.bits).require.value + assert(init.features === features) + assert(initCodec.encode(init).require.bytes === bin) + } + } + test("encode/decode live node_announcements") { val ann = hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce2690001025acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607" val bin = ann.bits @@ -77,7 +95,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 +145,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 +163,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", From 2a50f174c1fd059e1aa00216448bfa77fe2069ed Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 12 Dec 2019 17:16:35 +0100 Subject: [PATCH 2/8] Remove allowMultiPart API field We instead rely on the MPP feature being set in nodeParams. That means MPP-enabled nodes need to update their reference.conf. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 14 +-- .../payment/receive/MultiPartHandler.scala | 7 +- .../fr/acinq/eclair/EclairImplSpec.scala | 19 ++-- .../eclair/integration/IntegrationSpec.scala | 12 +-- .../eclair/payment/MultiPartHandlerSpec.scala | 91 ++++++++++--------- .../scala/fr/acinq/eclair/api/Service.scala | 6 +- 6 files changed, 75 insertions(+), 74 deletions(-) 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/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 3dd1d67f97..b036593be9 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) @@ -63,6 +63,7 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu // Once we're confident most of the network has upgraded, we should switch to mandatory payment secrets. val features = { val f1 = Seq(Features.PAYMENT_SECRET_OPTIONAL) + val allowMultiPart = Features.hasFeature(nodeParams.features, Features.BASIC_MULTI_PART_PAYMENT_OPTIONAL) || Features.hasFeature(nodeParams.features, Features.BASIC_MULTI_PART_PAYMENT_MANDATORY) 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 Some(PaymentRequest.Features(f1 ++ f2 ++ f3: _*)) @@ -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/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/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 89d93fd0fb..6162623ff5 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 @@ -149,8 +149,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService 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("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)) @@ -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) 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-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 { From 97adf0b605bc8f222d80a80aaec672594e9f1792 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Tue, 17 Dec 2019 18:57:35 +0100 Subject: [PATCH 3/8] Rework features Add types to allow cleaner dependency validation. Most of the time we don't care whether a feature is activated as optional or mandatory, which caused duplicate code. This is now handled more cleanly. It also paves the way to annotate features with the places they should be advertised (Init vs NodeAnn vs ChannelAnn vs invoice). --- .../main/scala/fr/acinq/eclair/Features.scala | 142 +++++++++++++----- .../scala/fr/acinq/eclair/NodeParams.scala | 5 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 14 +- .../acinq/eclair/payment/PaymentPacket.scala | 4 +- .../acinq/eclair/payment/PaymentRequest.scala | 18 +-- .../payment/receive/MultiPartHandler.scala | 8 +- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 84 ++++++++--- .../scala/fr/acinq/eclair/StartupSpec.scala | 43 +++--- .../eclair/payment/PaymentInitiatorSpec.scala | 8 +- .../eclair/payment/PaymentPacketSpec.scala | 6 +- .../eclair/payment/PaymentRequestSpec.scala | 38 ++--- .../eclair/router/AnnouncementsSpec.scala | 2 +- 12 files changed, 233 insertions(+), 139 deletions(-) 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..dd265b4db8 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,123 @@ 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 { + val rfcName: String + val mandatory: Int + val optional: Int + + 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 optional = 1 + } - 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 optional = 3 + } - val VARIABLE_LENGTH_ONION_MANDATORY = 8 - val VARIABLE_LENGTH_ONION_OPTIONAL = 9 + case object ChannelRangeQueries extends Feature { + val rfcName = "gossip_queries" + val mandatory = 6 + val optional = 7 + } - val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 10 - val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 11 + case object VariableLengthOnion extends Feature { + val rfcName = "var_onion_optin" + val mandatory = 8 + val optional = 9 + } - val PAYMENT_SECRET_MANDATORY = 14 - val PAYMENT_SECRET_OPTIONAL = 15 + case object ChannelRangeQueriesExtended extends Feature { + val rfcName = "gossip_queries_ex" + val mandatory = 10 + val optional = 11 + } - val BASIC_MULTI_PART_PAYMENT_MANDATORY = 16 - val BASIC_MULTI_PART_PAYMENT_OPTIONAL = 17 + case object PaymentSecret extends Feature { + val rfcName = "payment_secret" + val mandatory = 14 + val optional = 15 + } + + case object BasicMultiPartPayment extends Feature { + val rfcName = "basic_mpp" + val mandatory = 16 + val optional = 17 + } // 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 + val optional = 51 + } + + // 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, None) && dependencies.exists(d => !hasFeature(features, d, None)) => + FeatureException(s"${features.toBin} sets $feature but is missing a dependency (${dependencies.mkString(" or ")})") + } + + 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 = if (features.sizeLessThanOrEqual(bit)) false else features.reverse.get(bit) - def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(features.bits, bit) + def hasFeature(features: BitVector, feature: Feature, support: Option[FeatureSupport]): 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, 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 +145,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 3c46a53f23..6086e62e0a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -173,6 +173,9 @@ object NodeParams { val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") + val features = ByteVector.fromValidHex(config.getString("features")) + Features.validateFeatureGraph(features).foreach(e => throw e) + val overrideFeatures: Map[PublicKey, ByteVector] = config.getConfigList("override-features").map { e => val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) val f = ByteVector.fromValidHex(e.getString("features")) @@ -221,7 +224,7 @@ object NodeParams { alias = nodeAlias, color = Color(color(0), color(1), color(2)), publicAddresses = addresses, - features = ByteVector.fromValidHex(config.getString("features")), + features = features, overrideFeatures = overrideFeatures, syncWhitelist = syncWhitelist, dustLimit = dustLimitSatoshis, 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 7193de3811..f97a6bb616 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 @@ -139,16 +139,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A if (Features.areSupported(remoteInit.features)) { d.origin_opt.foreach(origin => origin ! "connected") - import Features._ + def hasLocalFeature(f: Feature) = Features.hasFeature(d.localInit.features, f, None) - def hasLocalFeature(bit: Int) = Features.hasFeature(d.localInit.features, bit) - - def hasRemoteFeature(bit: Int) = Features.hasFeature(remoteInit.features, 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 hasRemoteFeature(f: Feature) = Features.hasFeature(remoteInit.features, f, None) + val canUseChannelRangeQueries = hasLocalFeature(Features.ChannelRangeQueries) && hasRemoteFeature(Features.ChannelRangeQueries) + val canUseChannelRangeQueriesEx = hasLocalFeature(Features.ChannelRangeQueriesExtended) && hasRemoteFeature(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 @@ -159,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 (hasRemoteFeature(Features.InitialRoutingSync)) { // "old" nodes, do as before log.info("peer requested a full routing table dump") router ! GetRoutingState 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..325e4677af 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, None) => 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..9065c8b4d3 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,7 @@ 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") - } + validateFeatureGraph(features.bitmask).foreach(e => throw e) 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 +128,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 +328,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, None) + lazy val allowPaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF, None) + lazy val requirePaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF, Some(FeatureSupport.Mandatory)) + lazy val allowTrampoline: Boolean = hasFeature(bitmask, TrampolinePayment, None) 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 b036593be9..919d90581e 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 @@ -62,10 +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 allowMultiPart = Features.hasFeature(nodeParams.features, Features.BASIC_MULTI_PART_PAYMENT_OPTIONAL) || Features.hasFeature(nodeParams.features, Features.BASIC_MULTI_PART_PAYMENT_MANDATORY) - 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, None) + 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) 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..6ff2efea62 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, None)) + assert(hasFeature(features, InitialRoutingSync, None)) + assert(hasFeature(features, VariableLengthOnion, None)) } 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, None)) + 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 436d2d02f4..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 +import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Block import fr.acinq.eclair.crypto.LocalKeyManager import org.scalatest.FunSuite @@ -28,16 +28,19 @@ 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)") { @@ -54,29 +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") { - val blockCount = new AtomicLong(0) - val keyManager = new LocalKeyManager(seed = randomBytes32, chainHash = Block.TestnetGenesisBlock.hash) - val feeEstimator = new TestConstants.TestFeeEstimator - val db = TestConstants.inMemoryDb() - for (deprecated <- Seq("global-features", "local-features")) { val illegalGlobalFeaturesConf = ConfigFactory.parseString(deprecated + " = \"0200\"") - val conf = illegalGlobalFeaturesConf.withFallback(ConfigFactory.parseResources("reference.conf").getConfig("eclair")) + val conf = illegalGlobalFeaturesConf.withFallback(defaultConf) - val nodeParamsAttempt = Try(NodeParams.makeNodeParams(conf, keyManager, None, db, blockCount, feeEstimator)) + 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/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index bcd7d842cf..429972e79e 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 @@ -202,7 +202,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun 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 745ea9f65a..0809b5cf33 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 @@ -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) @@ -378,9 +378,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll { object PaymentPacketSpec { - import fr.acinq.eclair.Features.VARIABLE_LENGTH_ONION_OPTIONAL - - val variableLengthOnionFeature = ByteVector.fromLong(1L << VARIABLE_LENGTH_ONION_OPTIONAL) + 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 = { 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 122e8a039b..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 @@ -49,7 +49,7 @@ 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.features) - assert(Features.hasFeature(ann.features, Features.VARIABLE_LENGTH_ONION_OPTIONAL)) + assert(Features.hasFeature(ann.features, Features.VariableLengthOnion, Some(FeatureSupport.Optional))) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } From 120f773267640eb0491a30b05137e905b1a3f629 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Wed, 18 Dec 2019 15:19:01 +0100 Subject: [PATCH 4/8] Use new feature types in Trampoline code --- eclair-core/src/main/resources/reference.conf | 10 +++++----- .../main/scala/fr/acinq/eclair/Features.scala | 16 ++++------------ .../scala/fr/acinq/eclair/NodeParams.scala | 4 +++- .../main/scala/fr/acinq/eclair/io/Peer.scala | 10 +++++----- .../acinq/eclair/payment/PaymentRequest.scala | 3 ++- .../eclair/payment/relay/NodeRelayer.scala | 3 +-- .../eclair/integration/IntegrationSpec.scala | 18 +++++++++--------- .../acinq/eclair/payment/NodeRelayerSpec.scala | 3 ++- .../eclair/payment/PaymentInitiatorSpec.scala | 2 +- .../eclair/payment/PaymentPacketSpec.scala | 2 +- 10 files changed, 33 insertions(+), 38 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 27d42f0b04..533664d9b4 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -39,10 +39,10 @@ eclair { trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag features = "0a8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex + variable_length_onion override-features = [ // optional per-node features - # { - # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - # 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 @@ -130,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/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index dd265b4db8..880c757dba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -32,9 +32,9 @@ object FeatureSupport { // @formatter:on sealed trait Feature { - val rfcName: String - val mandatory: Int - val optional: Int + def rfcName: String + def mandatory: Int + def optional: Int = mandatory + 1 override def toString = rfcName } @@ -44,44 +44,37 @@ object Features { case object OptionDataLossProtect extends Feature { val rfcName = "option_data_loss_protect" val mandatory = 0 - val optional = 1 } 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 optional = 3 } case object ChannelRangeQueries extends Feature { val rfcName = "gossip_queries" val mandatory = 6 - val optional = 7 } case object VariableLengthOnion extends Feature { val rfcName = "var_onion_optin" val mandatory = 8 - val optional = 9 } case object ChannelRangeQueriesExtended extends Feature { val rfcName = "gossip_queries_ex" val mandatory = 10 - val optional = 11 } case object PaymentSecret extends Feature { val rfcName = "payment_secret" val mandatory = 14 - val optional = 15 } case object BasicMultiPartPayment extends Feature { val rfcName = "basic_mpp" val mandatory = 16 - val optional = 17 } // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) @@ -90,7 +83,6 @@ object Features { case object TrampolinePayment extends Feature { val rfcName = "trampoline_payment" val mandatory = 50 - val optional = 51 } // Features may depend on other features, as specified in Bolt 9. @@ -105,7 +97,7 @@ object Features { def validateFeatureGraph(features: BitVector): Option[FeatureException] = featuresDependency.collectFirst { case (feature, dependencies) if hasFeature(features, feature, None) && dependencies.exists(d => !hasFeature(features, d, None)) => - FeatureException(s"${features.toBin} sets $feature but is missing a dependency (${dependencies.mkString(" or ")})") + FeatureException(s"${features.toBin} sets $feature but is missing a dependency (${dependencies.filter(d => !hasFeature(features, d, None)).mkString(" and ")})") } def validateFeatureGraph(features: ByteVector): Option[FeatureException] = validateFeatureGraph(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 6086e62e0a..ef70452cd1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -83,6 +83,7 @@ case class NodeParams(keyManager: KeyManager, enableTrampolinePayment: Boolean) { val privateKey = keyManager.nodeKey.privateKey val nodeId = keyManager.nodeId + def currentBlockHeight: Long = blockCount.get } @@ -174,7 +175,8 @@ object NodeParams { require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") val features = ByteVector.fromValidHex(config.getString("features")) - Features.validateFeatureGraph(features).foreach(e => throw e) + 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"))) 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 f97a6bb616..328a1302c1 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 @@ -139,12 +139,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A if (Features.areSupported(remoteInit.features)) { d.origin_opt.foreach(origin => origin ! "connected") - def hasLocalFeature(f: Feature) = Features.hasFeature(d.localInit.features, f, None) + def localHasFeature(f: Feature): Boolean = Features.hasFeature(d.localInit.features, f, None) - def hasRemoteFeature(f: Feature) = Features.hasFeature(remoteInit.features, f, None) + def remoteHasFeature(f: Feature): Boolean = Features.hasFeature(remoteInit.features, f, None) - val canUseChannelRangeQueries = hasLocalFeature(Features.ChannelRangeQueries) && hasRemoteFeature(Features.ChannelRangeQueries) - val canUseChannelRangeQueriesEx = hasLocalFeature(Features.ChannelRangeQueriesExtended) && hasRemoteFeature(Features.ChannelRangeQueriesExtended) + 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 @@ -155,7 +155,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A } else { log.info("not syncing with this peer") } - } else if (hasRemoteFeature(Features.InitialRoutingSync)) { + } else if (remoteHasFeature(Features.InitialRoutingSync)) { // "old" nodes, do as before log.info("peer requested a full routing table dump") router ! GetRoutingState 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 9065c8b4d3..3209f36863 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 @@ -45,7 +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") - validateFeatureGraph(features.bitmask).foreach(e => throw e) + 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") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index a73dd55b56..41923f3a24 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -21,7 +21,6 @@ import java.util.UUID import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, PoisonPill, Props} import akka.event.Logging.MDC import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.Features.{BASIC_MULTI_PART_PAYMENT_MANDATORY, BASIC_MULTI_PART_PAYMENT_OPTIONAL} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream} import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment @@ -141,7 +140,7 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c case Some(invoiceFeatures) => log.debug("relaying trampoline payment to non-trampoline recipient") val routingHints = payloadOut.invoiceRoutingInfo.map(_.map(_.toSeq).toSeq).getOrElse(Nil) - val allowMultiPart = Features.hasFeature(invoiceFeatures, BASIC_MULTI_PART_PAYMENT_OPTIONAL) || Features.hasFeature(invoiceFeatures, BASIC_MULTI_PART_PAYMENT_MANDATORY) + val allowMultiPart = Features.hasFeature(invoiceFeatures, Features.BasicMultiPartPayment, None) val payFSM = spawnOutgoingPayFSM(paymentCfg, allowMultiPart) if (allowMultiPart) { if (payloadOut.paymentSecret.isEmpty) { 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 6162623ff5..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("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)) @@ -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/payment/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala index d73c2116ab..1f1cb04944 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 @@ -259,7 +259,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 429972e79e..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 @@ -195,7 +195,7 @@ 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 => 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 0809b5cf33..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 @@ -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)) } From 9c595e3c42f07042d95bbae90360a4b9dd2ed00f Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 19 Dec 2019 11:34:14 +0100 Subject: [PATCH 5/8] Combine feature in codec, not type This allows us to drop the global/local fields from the codebase. They are handled directly at the wire level. --- .../fr/acinq/eclair/channel/Channel.scala | 6 ++-- .../acinq/eclair/channel/ChannelTypes.scala | 6 ++-- .../fr/acinq/eclair/channel/Helpers.scala | 30 +++++++++---------- .../main/scala/fr/acinq/eclair/io/Peer.scala | 11 ++++--- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 6 ++-- .../eclair/wire/LightningMessageCodecs.scala | 24 ++++++++++++--- .../eclair/wire/LightningMessageTypes.scala | 15 +--------- .../fr/acinq/eclair/channel/FuzzyPipe.scala | 8 ++--- .../fr/acinq/eclair/channel/FuzzySpec.scala | 4 +-- .../acinq/eclair/channel/RecoverySpec.scala | 4 +-- .../acinq/eclair/channel/ThroughputSpec.scala | 4 +-- .../states/StateTestsHelperMethods.scala | 4 +-- .../a/WaitForAcceptChannelStateSpec.scala | 4 +-- .../a/WaitForOpenChannelStateSpec.scala | 4 +-- ...itForFundingCreatedInternalStateSpec.scala | 4 +-- .../b/WaitForFundingCreatedStateSpec.scala | 4 +-- .../b/WaitForFundingSignedStateSpec.scala | 4 +-- .../c/WaitForFundingConfirmedStateSpec.scala | 8 ++--- .../c/WaitForFundingLockedStateSpec.scala | 4 +-- .../channel/states/e/OfflineStateSpec.scala | 4 +-- .../channel/states/h/ClosingStateSpec.scala | 8 ++--- .../interop/rustytests/RustyTestsSpec.scala | 4 +-- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 14 ++++----- .../MultiPartPaymentLifecycleSpec.scala | 4 +-- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 26 +++++++++------- .../wire/LightningMessageCodecsSpec.scala | 18 +++++------ 26 files changed, 117 insertions(+), 115 deletions(-) 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 98873a9263..0abc97fa4c 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 @@ -307,8 +307,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 } @@ -340,8 +339,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 328a1302c1..44cd70f3f5 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 @@ -101,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(f) => wire.Init(ByteVector.empty, f) - case None => wire.Init(ByteVector.empty, nodeParams.features) + 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) { @@ -134,7 +134,7 @@ 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.features)) { d.origin_opt.foreach(origin => origin ! "connected") @@ -680,8 +680,7 @@ object Peer { maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, defaultFinalScriptPubKey = defaultFinalScriptPubKey, isFunder = isFunder, - globalFeatures = ByteVector.empty, - localFeatures = nodeParams.features) + features = nodeParams.features) } /** 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 3680975cab..64414b8543 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 @@ -20,18 +20,34 @@ 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.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.HNil /** * Created by PM on 15/11/2016. */ object LightningMessageCodecs { - val initCodec: Codec[Init] = ( - ("globalFeatures" | varsizebinarydata) :: - ("localFeatures" | varsizebinarydata)).as[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)).exmap[ByteVector]( + features => { + val f1 = features.head + val f2 = features.tail.head + val combinedFeatures = if (f1.length == f2.length) { + f1 | f2 + } else if (f1.length > f2.length) { + f1 | f2.padLeft(f1.length) + } else { + f1.padLeft(f2.length) | f2 + } + Attempt.Successful(combinedFeatures) + }, + features => Attempt.Successful(ByteVector.empty :: features :: HNil) + ) + + val initCodec: Codec[Init] = combinedFeaturesCodec.as[Init] val errorCodec: Codec[Error] = ( ("channelId" | bytes32) :: 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 43482b8ee2..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,20 +45,7 @@ sealed trait HasChainHash extends LightningMessage { def chainHash: ByteVector32 sealed trait UpdateMessage extends HtlcMessage // <- not in the spec // @formatter:on -/** For historical reasons, features are divided into two feature bitmasks. We only use the second one in our messages, but we allow receiving in both. */ -case class Init(globalFeatures: ByteVector, localFeatures: ByteVector) extends SetupMessage { - val features = combineFeatures(globalFeatures, localFeatures) - - private def combineFeatures(f1: ByteVector, f2: ByteVector): ByteVector = { - if (f1.length == f2.length) { - f1 | f2 - } else if (f1.length > f2.length) { - f1 | f2.padLeft(f1.length) - } else { - f1.padLeft(f2.length) | f2 - } - } -} +case class Init(features: ByteVector) extends SetupMessage 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/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 afdc956a93..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 @@ -34,9 +34,9 @@ class RecoverySpec extends TestkitBaseClass with StateTestsHelperMethods { } } - def aliceInit = Init(ByteVector.empty, TestConstants.Alice.nodeParams.features) + def aliceInit = Init(TestConstants.Alice.nodeParams.features) - def bobInit = Init(ByteVector.empty, TestConstants.Bob.nodeParams.features) + 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 cfe3519d82..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 @@ -60,9 +60,9 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } - def aliceInit = Init(ByteVector.empty, TestConstants.Alice.nodeParams.features) + def aliceInit = Init(TestConstants.Alice.nodeParams.features) - def bobInit = Init(ByteVector.empty, TestConstants.Bob.nodeParams.features) + def bobInit = Init(TestConstants.Bob.nodeParams.features) /** * This test checks the case where a disconnection occurs *right before* the counterparty receives a new sig 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 d4519fa26b..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(ByteVector.empty, TestConstants.Alice.nodeParams.features) - val bobInit = Init(ByteVector.empty, TestConstants.Bob.nodeParams.features) + 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/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 466c509c28..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 @@ -31,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, _} @@ -75,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(ByteVector.empty, Bob.nodeParams.features), 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)) @@ -213,7 +213,7 @@ 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] - transport.send(peer, wire.Init(ByteVector.empty, bin"01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00".bytes)) + transport.send(peer, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) } @@ -226,7 +226,7 @@ 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] - transport.send(peer, wire.Init(bin"01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00".bytes, ByteVector.empty)) + transport.send(peer, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) } @@ -276,19 +276,19 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { test("sync if no whitelist is defined") { f => import f._ - val remoteInit = wire.Init(ByteVector.empty, bin"10000000".bytes) // bob supports 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(hex"0200", bin"10000000".bytes) // bob supports channel range queries and variable length onion + 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(ByteVector.empty, bin"10000000".bytes) // bob supports 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) } 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/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 caff51decd..22e5d46087 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 @@ -45,19 +45,19 @@ class LightningMessageCodecsSpec extends FunSuite { test("encode/decode init message") { val testCases = Seq( - (hex"0000 0000", hex""), // no features - (hex"0000 0002088a", hex"088a"), // no global features - (hex"00020200 0000", hex"0200"), // no local features - (hex"00020200 0002088a", hex"0a8a"), // local and global - no conflict - same size - (hex"00020200 0003020002", hex"020202"), // local and global - no conflict - different sizes - (hex"00020a02 0002088a", hex"0a8a"), // local and global - conflict - same size - (hex"00022200 000302aaa2", hex"02aaa2") // local and global - conflict - different sizes + (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) <- testCases) { + for ((bin, features, encoded) <- testCases) { val init = initCodec.decode(bin.bits).require.value assert(init.features === features) - assert(initCodec.encode(init).require.bytes === bin) + assert(initCodec.encode(init).require.bytes === encoded) } } From 1b2daaab078ed6ef7090ba677705aa0e4e252673 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 19 Dec 2019 14:07:28 +0100 Subject: [PATCH 6/8] Fix an unrelated flaky test --- .../src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From ea7abd92e7fb13e1c74404b1ba4c0e4712517b6d Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 9 Jan 2020 09:24:50 +0100 Subject: [PATCH 7/8] Refactoring from PR comments --- .../main/scala/fr/acinq/eclair/Features.scala | 8 +++++-- .../eclair/wire/LightningMessageCodecs.scala | 22 ++++++------------- .../wire/LightningMessageCodecsSpec.scala | 1 + 3 files changed, 14 insertions(+), 17 deletions(-) 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 880c757dba..c7efab245d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -33,7 +33,9 @@ object FeatureSupport { sealed trait Feature { def rfcName: String + def mandatory: Int + def optional: Int = mandatory + 1 override def toString = rfcName @@ -105,14 +107,16 @@ object Features { // Note that BitVector indexes from left to right whereas the specification indexes from right to left. // This is why we have to reverse the bits to check if a feature is set. - private def hasFeature(features: BitVector, bit: Int): Boolean = 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: BitVector, feature: Feature, support: Option[FeatureSupport]): Boolean = support match { + def hasFeature(features: BitVector, feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match { case Some(FeatureSupport.Mandatory) => hasFeature(features, feature.mandatory) case Some(FeatureSupport.Optional) => hasFeature(features, feature.optional) case None => hasFeature(features, feature.optional) || hasFeature(features, feature.mandatory) } + def hasFeature(features: ByteVector, feature: Feature): Boolean = hasFeature(features.bits, feature, None) + def hasFeature(features: ByteVector, feature: Feature, support: Option[FeatureSupport]): Boolean = hasFeature(features.bits, feature, support) /** 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 64414b8543..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 @@ -23,7 +23,6 @@ import kamon.tag.TagSet import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} -import shapeless.HNil /** * Created by PM on 15/11/2016. @@ -31,21 +30,14 @@ import shapeless.HNil object LightningMessageCodecs { /** 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)).exmap[ByteVector]( - features => { - val f1 = features.head - val f2 = features.tail.head - val combinedFeatures = if (f1.length == f2.length) { - f1 | f2 - } else if (f1.length > f2.length) { - f1 | f2.padLeft(f1.length) - } else { - f1.padLeft(f2.length) | f2 - } - Attempt.Successful(combinedFeatures) + val combinedFeaturesCodec: Codec[ByteVector] = ( + ("globalFeatures" | varsizebinarydata) :: + ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[ByteVector]( + { case (gf, lf) => + val length = gf.length.max(lf.length) + gf.padLeft(length) | lf.padLeft(length) }, - features => Attempt.Successful(ByteVector.empty :: features :: HNil) - ) + { features => (ByteVector.empty, features) }) val initCodec: Codec[Init] = combinedFeaturesCodec.as[Init] 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 22e5d46087..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 @@ -58,6 +58,7 @@ class LightningMessageCodecsSpec extends FunSuite { 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) } } From 9bd30599a88b2a0e9ff6676604be29ca6786294d Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 9 Jan 2020 11:12:32 +0100 Subject: [PATCH 8/8] Remove redundant default argument of None for hasFeature --- eclair-core/src/main/scala/fr/acinq/eclair/Features.scala | 6 +++--- eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala | 4 ++-- .../main/scala/fr/acinq/eclair/payment/PaymentPacket.scala | 2 +- .../main/scala/fr/acinq/eclair/payment/PaymentRequest.scala | 6 +++--- .../fr/acinq/eclair/payment/receive/MultiPartHandler.scala | 2 +- .../src/test/scala/fr/acinq/eclair/FeaturesSpec.scala | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) 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 c7efab245d..7f9ea52bd1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -98,8 +98,8 @@ object Features { case class FeatureException(message: String) extends IllegalArgumentException(message) def validateFeatureGraph(features: BitVector): Option[FeatureException] = featuresDependency.collectFirst { - case (feature, dependencies) if hasFeature(features, feature, None) && dependencies.exists(d => !hasFeature(features, d, None)) => - FeatureException(s"${features.toBin} sets $feature but is missing a dependency (${dependencies.filter(d => !hasFeature(features, d, None)).mkString(" and ")})") + 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) @@ -115,7 +115,7 @@ object Features { case None => hasFeature(features, feature.optional) || hasFeature(features, feature.mandatory) } - def hasFeature(features: ByteVector, feature: Feature): Boolean = hasFeature(features.bits, feature, None) + 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) 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 44cd70f3f5..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 @@ -139,9 +139,9 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A if (Features.areSupported(remoteInit.features)) { d.origin_opt.foreach(origin => origin ! "connected") - def localHasFeature(f: Feature): Boolean = Features.hasFeature(d.localInit.features, f, None) + def localHasFeature(f: Feature): Boolean = Features.hasFeature(d.localInit.features, f) - def remoteHasFeature(f: Feature): Boolean = Features.hasFeature(remoteInit.features, f, None) + 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) 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 325e4677af..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 @@ -58,7 +58,7 @@ object IncomingPacket { packetType.peel(privateKey, add.paymentHash, packet) match { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => OnionCodecs.perHopPayloadCodecByPacketType(packetType, p.isLastPacket).decode(payload.bits) match { - case Attempt.Successful(DecodeResult(_: Onion.TlvFormat, _)) if !Features.hasFeature(features, Features.VariableLengthOnion, None) => 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 3209f36863..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 @@ -329,10 +329,10 @@ object PaymentRequest { */ case class Features(bitmask: BitVector) extends TaggedField { lazy val supported: Boolean = areSupported(bitmask) - lazy val allowMultiPart: Boolean = hasFeature(bitmask, BasicMultiPartPayment, None) - lazy val allowPaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF, None) + 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, None) + 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 919d90581e..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 @@ -63,7 +63,7 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu // Once we're confident most of the network has upgraded, we should switch to mandatory payment secrets. val features = { val f1 = Seq(Features.PaymentSecret.optional, Features.VariableLengthOnion.optional) - val allowMultiPart = Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment, None) + 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: _*)) 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 6ff2efea62..efa1cd205c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -39,13 +39,13 @@ class FeaturesSpec extends FunSuite { test("'initial_routing_sync', 'data_loss_protect' and 'variable_length_onion' features") { val features = hex"010a" assert(areSupported(features)) - assert(hasFeature(features, OptionDataLossProtect, None)) + assert(hasFeature(features, OptionDataLossProtect)) assert(hasFeature(features, InitialRoutingSync, None)) - assert(hasFeature(features, VariableLengthOnion, None)) + assert(hasFeature(features, VariableLengthOnion)) } test("'variable_length_onion' feature") { - assert(hasFeature(hex"0100", VariableLengthOnion, None)) + 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)))