From 6e2d30f913c4c7ecc4f06f5876957cc529f255e2 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 10 Sep 2018 23:02:35 +0200 Subject: [PATCH 01/86] Add extended query messages Extended query messages are allow peers to exchange short channel id and timestamps, and to query either announcement + updates or just updates: - query_channel_range_ex is used asked to ask for a list of (short channel ids + timestamp) - reply_channel_range_ex will return a list of (short channel ids + timestamp) - query_short_channel_ids_ex is used to ask for channel updates and optionaly the matching channel announcement - query_short_channel_ids_end_ex is sent when an extended query has been completed --- .../eclair/router/ChannelRangeQueries.scala | 12 +- .../eclair/router/ChannelRangeQueriesEx.scala | 126 +++++++++++++++ .../scala/fr/acinq/eclair/router/Router.scala | 150 ++++++++++++++++-- .../eclair/wire/LightningMessageCodecs.scala | 29 ++++ .../eclair/wire/LightningMessageTypes.scala | 44 +++++ .../router/ChannelRangeQueriesExSpec.scala | 39 +++++ .../eclair/router/RoutingSyncExSpec.scala | 135 ++++++++++++++++ .../acinq/eclair/router/RoutingSyncSpec.scala | 1 - 8 files changed, 516 insertions(+), 20 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala index 6a662e049a..1162757bd3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala @@ -48,14 +48,12 @@ object ChannelRangeQueries { def encodeShortChannelIdsSingle(shortChannelIds: Iterable[ShortChannelId], format: Byte, useGzip: Boolean): BinaryData = { val bos = new ByteArrayOutputStream() bos.write(format) - format match { - case UNCOMPRESSED_FORMAT => - shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, bos, ByteOrder.BIG_ENDIAN)) - case ZLIB_FORMAT => - val output = if (useGzip) new GZIPOutputStream(bos) else new DeflaterOutputStream(bos) - shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, output, ByteOrder.BIG_ENDIAN)) - output.finish() + val out = format match { + case UNCOMPRESSED_FORMAT => bos + case ZLIB_FORMAT => if (useGzip) new GZIPOutputStream(bos) else new DeflaterOutputStream(bos) } + shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN)) + out.close() bos.toByteArray } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala new file mode 100644 index 0000000000..09eb1644e9 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala @@ -0,0 +1,126 @@ +package fr.acinq.eclair.router + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} +import java.nio.ByteOrder +import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream} + +import fr.acinq.bitcoin.{BinaryData, Protocol} +import fr.acinq.eclair.ShortChannelId + +import scala.annotation.tailrec +import scala.collection.SortedSet +import scala.collection.immutable.SortedMap + +object ChannelRangeQueriesEx { + val UNCOMPRESSED_FORMAT = 0.toByte + val ZLIB_FORMAT = 1.toByte + + case class ShortChannelIdAndTimestampsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: BinaryData) + + /** + * Compressed a sequence of *sorted* short channel id. + * + * @param shortChannelIds must be sorted beforehand + * @return a sequence of encoded short channel ids + */ + def encodeShortChannelIdAndTimestamps(firstBlockIn: Long, numBlocksIn: Long, + shortChannelIds: SortedSet[ShortChannelId], timestamp: ShortChannelId => Long, + format: Byte): List[ShortChannelIdAndTimestampsBlock] = { + if (shortChannelIds.isEmpty) { + // special case: reply with an "empty" block + List(ShortChannelIdAndTimestampsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) + } else { + // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + val count = format match { + case UNCOMPRESSED_FORMAT => 4000 + case ZLIB_FORMAT => 8000 // TODO: do something less simplistic... + } + shortChannelIds.grouped(count).map(ids => { + val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { + val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight + val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 + (firstBlock, numBlocks) + } + val encoded = encodeShortChannelIdAndTimestampsSingle(ids, timestamp, format) + ShortChannelIdAndTimestampsBlock(firstBlock, numBlocks, encoded) + }).toList + } + } + + def encodeShortChannelIdAndTimestampsSingle(shortChannelIds: Iterable[ShortChannelId], timestamp: ShortChannelId => Long, format: Byte): BinaryData = { + val bos = new ByteArrayOutputStream() + bos.write(format) + val out = format match { + case UNCOMPRESSED_FORMAT => bos + case ZLIB_FORMAT => new DeflaterOutputStream(bos) + } + shortChannelIds.foreach(id => { + Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN) + Protocol.writeUInt32(timestamp(id), out, ByteOrder.BIG_ENDIAN) + }) + out.close() + bos.toByteArray + } + + def encodeShortChannelIdAndFlagSingle(shortChannelIds: Iterable[ShortChannelId], flag: ShortChannelId => Byte, format: Byte): BinaryData = { + val bos = new ByteArrayOutputStream() + bos.write(format) + val out = format match { + case UNCOMPRESSED_FORMAT => bos + case ZLIB_FORMAT => new DeflaterOutputStream(bos) + } + shortChannelIds.foreach(id => { + Protocol.writeUInt64(id.toLong, bos, ByteOrder.BIG_ENDIAN) + Protocol.writeUInt8(flag(id), bos) + }) + out.close() + bos.toByteArray + } + + /** + * Decompress a zipped sequence of sorted [short channel id | timestamp] values. + * + * @param data + * @return a sorted map of short channel id -> timestamp + */ + def decodeShortChannelIdAndTimestamps(data: BinaryData): (Byte, SortedMap[ShortChannelId, Long]) = { + val format = data.head + if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, Long]) else { + val buffer = new Array[Byte](12) + + // read 12 bytes from input + // zipped input stream often returns less bytes than what you want to read + @tailrec + def read12(input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, 12 - offset) match { + case len if len <= 0 => len + case 12 => 12 + case len if offset + len == 12 => 12 + case len => read12(input, offset + len) + } + + + // read until there's nothing left + @tailrec + def loop(input: InputStream, acc: SortedMap[ShortChannelId, Long]): SortedMap[ShortChannelId, Long] = { + val check = read12(input) + if (check <= 0) acc else loop(input, acc + (ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) -> Protocol.uint32(buffer.drop(8), ByteOrder.BIG_ENDIAN))) + } + + def readAll() = { + val bis = new ByteArrayInputStream(data.tail.toArray) + val input = format match { + case UNCOMPRESSED_FORMAT => bis + case ZLIB_FORMAT => new InflaterInputStream(bis) + } + try { + (format, loop(input, SortedMap.empty[ShortChannelId, Long])) + } + finally { + input.close() + } + } + + readAll() + } + } +} 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 8c102799d9..82fea0356c 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 @@ -49,38 +49,53 @@ import scala.util.Try // @formatter:off case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) + case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) + case class RouteRequest(source: PublicKey, target: PublicKey, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[ChannelDesc] = Set.empty) -case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]) { require(hops.size > 0, "route cannot be empty") } + +case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]) { + require(hops.size > 0, "route cannot be empty") +} + case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed) case class LiftChannelExclusion(desc: ChannelDesc) + case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) + +case class SendChannelQueryEx(remoteNodeId: PublicKey, to: ActorRef) + case object GetRoutingState + case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) + case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) + case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) -case class Sync(missing: SortedSet[ShortChannelId], count: Int) +case class Sync(missing: SortedSet[ShortChannelId], count: Int, outdated: SortedSet[ShortChannelId] = SortedSet.empty[ShortChannelId], outdatedCount: Int = 0) case class DescEdge(desc: ChannelDesc, u: ChannelUpdate) extends DefaultWeightedEdge case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, ChannelAnnouncement], - updates: Map[ChannelDesc, ChannelUpdate], - stash: Stash, - rebroadcast: Rebroadcast, - awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done - privateChannels: Map[ShortChannelId, PublicKey], // short_channel_id -> node_id - privateUpdates: Map[ChannelDesc, ChannelUpdate], - excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure - graph: DirectedWeightedPseudograph[PublicKey, DescEdge], - sync: Map[PublicKey, Sync] + channels: SortedMap[ShortChannelId, ChannelAnnouncement], + updates: Map[ChannelDesc, ChannelUpdate], + stash: Stash, + rebroadcast: Rebroadcast, + awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done + privateChannels: Map[ShortChannelId, PublicKey], // short_channel_id -> node_id + privateUpdates: Map[ChannelDesc, ChannelUpdate], + excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure + graph: DirectedWeightedPseudograph[PublicKey, DescEdge], + sync: Map[PublicKey, Sync] ) sealed trait State + case object NORMAL extends State case object TickBroadcast + case object TickPruneStaleChannels // @formatter:on @@ -418,6 +433,17 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct remote ! filter stay + case Event(SendChannelQueryEx(remoteNodeId, remote), _) => + // ask for everything + val query = QueryChannelRangeEx(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + log.info("sending query_channel_range_ex={}", query) + remote ! query + + // we also set a pass-all filter for now (we can update it later) + val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) + remote ! filter + stay + // Warning: order matters here, this must be the first match for HasChainHash messages ! case Event(PeerRoutingMessage(remoteNodeId, routingMessage: HasChainHash), d) if routingMessage.chainHash != nodeParams.chainHash => sender ! TransportHandler.ReadAck(routingMessage) @@ -472,6 +498,17 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct replies.foreach(reply => sender ! reply) stay + case Event(PeerRoutingMessage(_, routingMessage@QueryChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + log.info("received query_channel_range_ex={}", routingMessage) + // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] + val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamp(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + log.info("sending back reply_channel_range_ex with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + val replies = blocks.map(block => ReplyChannelRangeEx(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + replies.foreach(reply => sender ! reply) + stay + case Event(PeerRoutingMessage(remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (format, theirShortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) @@ -486,6 +523,37 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct context.system.eventStream.publish(syncProgress(d1)) stay using d1 + case Event(PeerRoutingMessage(remoteNodeId, routingMessage@ReplyChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + val (format, theirTimestampMap) = ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(data) + val theirShortChannelIds = theirTimestampMap.keySet + // keep our ids that match [block, block + numberOfBlocks] + val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(id => { + val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) + height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) + }) + + // missing are the ones we don't have + val missing = theirShortChannelIds -- ourShortChannelIds + + // outdated are the ones for which our update timestamp is older that theirs + val outdated = ourShortChannelIds.filter(id => { + theirShortChannelIds.contains(id) && Router.getTimestamp(d.channels, d.updates)(id) < theirTimestampMap(id) + }) + log.info("received reply_channel_range_ex, we're missing {} channel announcements and we have {} outdated ones, format={} ", missing.size, outdated.size, format) + // we first sync missing channels, then outdated ones + val d1 = if (missing.nonEmpty) { + val (slice, rest) = missing.splitAt(SHORTID_WINDOW) + sender ! QueryShortChannelIdsEx(chainHash, 1.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip = false)) + d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size, outdated, outdated.size))) + } else if (outdated.nonEmpty) { + val (slice, rest) = outdated.splitAt(SHORTID_WINDOW) + sender ! QueryShortChannelIdsEx(chainHash, 0.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip = false)) + d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet.empty[ShortChannelId], 0, rest, outdated.size))) + } else d + context.system.eventStream.publish(syncProgress(d1)) + stay using d1 + case Event(PeerRoutingMessage(_, routingMessage@QueryShortChannelIds(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (_, shortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) @@ -502,6 +570,22 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct sender ! ReplyShortChannelIdsEnd(chainHash, 1) stay + case Event(PeerRoutingMessage(_, routingMessage@QueryShortChannelIdsEx(chainHash, flag, data)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + val (_, shortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) + log.info("received query_short_channel_ids_ex for {} channel announcements, useGzip={}", shortChannelIds.size, useGzip) + shortChannelIds.foreach(shortChannelId => { + d.channels.get(shortChannelId) match { + case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) + case Some(ca) => + if (flag == 1.toByte) sender ! ca + d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => sender ! u) + d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => sender ! u) + } + }) + sender ! ReplyShortChannelIdsEndEx(chainHash, 1) + stay + case Event(PeerRoutingMessage(remoteNodeId, routingMessage@ReplyShortChannelIdsEnd(chainHash, complete)), d) => sender ! TransportHandler.ReadAck(routingMessage) log.info("received reply_short_channel_ids_end={}", routingMessage) @@ -517,6 +601,27 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct } context.system.eventStream.publish(syncProgress(d1)) stay using d1 + + case Event(PeerRoutingMessage(remoteNodeId, routingMessage@ReplyShortChannelIdsEndEx(chainHash, complete)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + log.info("received reply_short_channel_ids_end_ex={}", routingMessage) + // have we more channels to ask this peer? + val d1 = d.sync.get(remoteNodeId) match { + case Some(sync) if sync.missing.nonEmpty => + log.info(s"asking {} for the next slice of missing short_channel_ids", remoteNodeId) + val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) + sender ! QueryShortChannelIdsEx(chainHash, 1.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) + case Some(sync) if sync.outdated.nonEmpty => + log.info(s"asking {} for the next slice of outdated short_channel_ids", remoteNodeId) + val (slice, rest) = sync.outdated.splitAt(SHORTID_WINDOW) + sender ! QueryShortChannelIdsEx(chainHash, 0.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = rest))) + case _ => + d + } + context.system.eventStream.publish(syncProgress(d1)) + stay using d1 } initialize() @@ -754,6 +859,27 @@ object Router { desc } + /** + * + * @param channels id -> announcement map + * @param updates channel updates + * @param id short channel id + * @return the timestamp of the most recent update for this channel id, 0 if we don't have any + */ + def getTimestamp(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(id: ShortChannelId): Long = { + val ca = channels(id) + val opt1 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)) + val opt2 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)) + val timestamp = (opt1, opt2) match { + case (Some(u1), Some(u2)) => Math.max(u1.timestamp, u2.timestamp) + case (Some(u1), None) => u1.timestamp + case (None, Some(u2)) => u2.timestamp + case (None, None) => + 0L + } + timestamp + } + /** * Routing fee have a variable part, as a simplification we compute fees using a default constant value for the amount */ 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 221a194c04..80d2bb4fff 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 @@ -318,6 +318,31 @@ object LightningMessageCodecs { ("data" | varsizebinarydata) ).as[ReplyChannelRange] + val queryShortChannelIdsExCodec: Codec[QueryShortChannelIdsEx] = ( + ("chainHash" | binarydata(32)) :: + ("flag" | byte) :: + ("data" | varsizebinarydata) + ).as[QueryShortChannelIdsEx] + + val replyShortChanelIdsEndExCodec: Codec[ReplyShortChannelIdsEndEx] = ( + ("chainHash" | binarydata(32)) :: + ("complete" | byte) + ).as[ReplyShortChannelIdsEndEx] + + val queryChannelRangeExCodec: Codec[QueryChannelRangeEx] = ( + ("chainHash" | binarydata(32)) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) + ).as[QueryChannelRangeEx] + + val replyChannelRangeExCodec: Codec[ReplyChannelRangeEx] = ( + ("chainHash" | binarydata(32)) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("complete" | byte) :: + ("data" | varsizebinarydata) + ).as[ReplyChannelRangeEx] + val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( ("chainHash" | binarydata(32)) :: ("firstTimestamp" | uint32) :: @@ -353,6 +378,10 @@ object LightningMessageCodecs { .typecase(263, queryChannelRangeCodec) .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) + .typecase(1001, queryShortChannelIdsExCodec) + .typecase(1002, replyShortChanelIdsEndExCodec) + .typecase(1003, queryChannelRangeExCodec) + .typecase(1004, replyChannelRangeExCodec) /** 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 227bcc4a82..45568c3f88 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 @@ -202,22 +202,66 @@ case class PerHopPayload(shortChannelId: ShortChannelId, amtToForward: Long, outgoingCltvValue: Long) +/** + * + * @param chainHash chain hash + * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded + */ case class QueryShortChannelIds(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash +/** + * + * @param chainHash chain hash + * @param flag if flag == 1, don't include send back channel announcements + * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded + */ +case class QueryShortChannelIdsEx(chainHash: BinaryData, + flag: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash + case class QueryChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash +case class QueryChannelRangeEx(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long) extends RoutingMessage with HasChainHash + +/** + * + * @param chainHash chain hash + * @param firstBlockNum first block that is found in data + * @param numberOfBlocks number of blocks spanned by data + * @param complete + * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded + */ case class ReplyChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, data: BinaryData) extends RoutingMessage with HasChainHash +/** + * + * @param chainHash chain hash + * @param firstBlockNum first block that is found in data + * @param numberOfBlocks number of blocks spanned by data + * @param complete + * @param data prefix + list of (short channel id + timestamp) values, where prefix specifies how the list is encoded + */ +case class ReplyChannelRangeEx(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash + case class ReplyShortChannelIdsEnd(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash +case class ReplyShortChannelIdsEndEx(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash + case class GossipTimestampFilter(chainHash: BinaryData, firstTimestamp: Long, timestampRange: Long) extends RoutingMessage with HasChainHash \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala new file mode 100644 index 0000000000..cf440caf69 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala @@ -0,0 +1,39 @@ +package fr.acinq.eclair.router + +import fr.acinq.bitcoin.Block +import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.router.ChannelRangeQueriesSpec.shortChannelIds +import fr.acinq.eclair.wire.{ReplyChannelRange, ReplyChannelRangeEx} +import org.junit.runner.RunWith +import org.scalatest.FunSuite +import org.scalatest.junit.JUnitRunner + +import scala.collection.immutable.SortedMap +import scala.util.Random + +@RunWith(classOf[JUnitRunner]) +class ChannelRangeQueriesExSpec extends FunSuite { + import ChannelRangeQueriesEx._ + val random = new Random() + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds + val timestamps = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap + + test("create `reply_channel_range_ex` messages (uncompressed format)") { + val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + var decoded = SortedMap.empty[ShortChannelId, Long] + replies.foreach(reply => decoded = decoded ++ ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(reply.data)._2) + assert(decoded.keySet == shortChannelIds) + shortChannelIds.foreach(id => timestamps(id) == decoded(id)) + } + + test("create `reply_channel_range_ex` messages (zlib format)") { + val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) + val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + var decoded = SortedMap.empty[ShortChannelId, Long] + replies.foreach(reply => decoded = decoded ++ ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(reply.data)._2) + assert(decoded.keySet == shortChannelIds) + shortChannelIds.foreach(id => timestamps(id) == decoded(id)) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala new file mode 100644 index 0000000000..491877b922 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala @@ -0,0 +1,135 @@ +package fr.acinq.eclair.router + +import akka.actor.{Actor, ActorRef, Props} +import akka.testkit.TestProbe +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction, TxOut} +import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult, WatchSpentBasic} +import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} +import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement, RoutingMessage} +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +import scala.concurrent.duration._ + +@RunWith(classOf[JUnitRunner]) +class RoutingSyncExSpec extends TestkitBaseClass { + + import RoutingSyncExSpec._ + + val txid = BinaryData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + + type FixtureParam = Tuple3[ActorRef, ActorRef, ActorRef] + + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(700) + + val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) + // A will be missing the last 100 items + val routingInfoA = fakeRoutingInfo.dropRight(200) + // and B will be missing the first 100 items + // and items 100 to 199 will have older timestamps + val routingInfoB = fakeRoutingInfo.drop(200).zipWithIndex.map { + case ((ca, u1, u2, na1, na2), i) if i >= 100 && i < 300 => (ca, u1.copy(timestamp = u1.timestamp - 100), u2.copy(timestamp = u2.timestamp - 100), na1, na2) + case (t, _) => t + } + + class FakeWatcher extends Actor { + def receive = { + case _: WatchSpentBasic => () + case ValidateRequest(ann) => + val txOut = TxOut(Satoshi(1000000), Script.pay2wsh(Scripts.multiSig2of2(ann.bitcoinKey1, ann.bitcoinKey2))) + val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(ann.shortChannelId) + sender ! ValidateResult(ann, Some(Transaction(version = 0, txIn = Nil, txOut = List.fill(outputIndex + 1)(txOut), lockTime = 0)), true, None) + case unexpected => println(s"unexpected : $unexpected") + } + } + + override def withFixture(test: OneArgTest) = { + val watcherA = system.actorOf(Props(new FakeWatcher())) + val paramsA = Alice.nodeParams + routingInfoA.map { + case (a, u1, u2, n1, n2) => + paramsA.networkDb.addChannel(a, txid, Satoshi(100000)) + paramsA.networkDb.addChannelUpdate(u1) + paramsA.networkDb.addChannelUpdate(u2) + paramsA.networkDb.addNode(n1) + paramsA.networkDb.addNode(n2) + } + val probe = TestProbe() + val switchboard = system.actorOf(Props(new Actor { + override def receive: Receive = { + case msg => probe.ref forward msg + } + }), "switchboard") + + val routerA = system.actorOf(Props(new Router(paramsA, watcherA)), "routerA") + val idA = PrivateKey(BinaryData("01" * 32), true).publicKey + + val watcherB = system.actorOf(Props(new FakeWatcher())) + val paramsB = Bob.nodeParams + routingInfoB.map { + case (a, u1, u2, n1, n2) => + paramsB.networkDb.addChannel(a, txid, Satoshi(100000)) + paramsB.networkDb.addChannelUpdate(u1) + paramsB.networkDb.addChannelUpdate(u2) + paramsB.networkDb.addNode(n1) + paramsB.networkDb.addNode(n2) + } + val routerB = system.actorOf(Props(new Router(paramsB, watcherB)), "routerB") + val idB = PrivateKey(BinaryData("02" * 32), true).publicKey + + val pipe = system.actorOf(Props(new RoutingSyncSpec.Pipe(routerA, idA, routerB, idA))) + val sender = TestProbe() + awaitCond({ + sender.send(routerA, 'channels) + val channelsA = sender.expectMsgType[Iterable[ChannelAnnouncement]] + channelsA.size == routingInfoA.size + }, max = 30 seconds) + + test((routerA, routerB, pipe)) + } + + test("initial sync") { + case (routerA, routerB, pipe) => { + Globals.blockCount.set(shortChannelIds.map(id => ShortChannelId.coordinates(id).blockHeight).max) + + val sender = TestProbe() + routerA ! SendChannelQueryEx(Alice.nodeParams.nodeId, pipe) + routerB ! SendChannelQueryEx(Bob.nodeParams.nodeId, pipe) + + awaitCond({ + sender.send(routerA, 'channels) + val channelsA = sender.expectMsgType[Iterable[ChannelAnnouncement]] + sender.send(routerB, 'channels) + val channelsB = sender.expectMsgType[Iterable[ChannelAnnouncement]] + channelsA.toSet == channelsB.toSet + }, max = 30 seconds) + } + } +} + +object RoutingSyncExSpec { + def makeFakeRoutingInfo(shortChannelId: ShortChannelId): (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement) = { + val (priv_a, priv_b, priv_funding_a, priv_funding_b) = (randomKey, randomKey, randomKey, randomKey) + val channelAnn_ab = channelAnnouncement(shortChannelId, priv_a, priv_b, priv_funding_a, priv_funding_b) + val TxCoordinates(blockHeight, _, _) = ShortChannelId.coordinates(shortChannelId) + val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_b.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, timestamp = blockHeight) + val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, priv_a.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, timestamp = blockHeight) + val nodeAnnouncement_a = makeNodeAnnouncement(priv_a, "a", Alice.nodeParams.color, List()) + val nodeAnnouncement_b = makeNodeAnnouncement(priv_b, "b", Bob.nodeParams.color, List()) + (channelAnn_ab, channelUpdate_ab, channelUpdate_ba, nodeAnnouncement_a, nodeAnnouncement_b) + } + + class Pipe(a: ActorRef, idA: PublicKey, b: ActorRef, idB: PublicKey) extends Actor { + def receive = { + case msg: RoutingMessage if sender == a => b ! PeerRoutingMessage(idA, msg) + case msg: RoutingMessage if sender == b => a ! PeerRoutingMessage(idB, msg) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 437202f6a3..2a0123d2b5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -4,7 +4,6 @@ import akka.actor.{Actor, ActorRef, Props} import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction, TxOut} -import fr.acinq.eclair import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult, WatchSpentBasic} From 14e72551bc752dd2c60902329b8d3b7ea55f8f3e Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 11 Sep 2018 14:47:17 +0200 Subject: [PATCH 02/86] Add a feature bit for extended channel range queries Last feature bit is currently 7, we use bit 15 to not interfere with new features being added to the LN spec. --- eclair-core/src/main/resources/reference.conf | 2 +- .../src/main/scala/fr/acinq/eclair/Features.scala | 2 ++ .../src/main/scala/fr/acinq/eclair/io/Peer.scala | 12 ++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index b2988f0086..d82d3d6ee1 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -41,7 +41,7 @@ eclair { node-alias = "eclair" node-color = "49daaa" global-features = "" - local-features = "8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + local-features = "808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex channel-flags = 1 // announce channels dust-limit-satoshis = 546 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 abecc33e06..0e247c39c9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -36,6 +36,8 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 + val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 14 + val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 15 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) 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 7eb18cf7af..cdda656a73 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 @@ -95,12 +95,17 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) + val remoteHasChannelRangeQueriesExOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) + val remoteHasChannelRangeQueriesExMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY) log.info(s"$remoteNodeId has features: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") if (Features.areSupported(remoteInit.localFeatures)) { origin_opt.map(origin => origin ! "connected") if (remoteHasInitialRoutingSync) { - if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { + // if they support extended channel queries we do nothing, they will send us their filters + log.info("{} has set initial routing sync and support extended channel range queries, we do nothing (they will send us a query)", remoteNodeId) + } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries we do nothing, they will send us their filters log.info("{} has set initial routing sync and support channel range queries, we do nothing (they will send us a query)", remoteNodeId) } else { @@ -108,7 +113,10 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor router ! GetRoutingState } } - if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { + // if they support extended channel queries, always ask for their filter + router ! SendChannelQueryEx(remoteNodeId, transport) + } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter router ! SendChannelQuery(remoteNodeId, transport) } From ada3bbbaaa6d5e64dc94067f2be15434f3bfa991 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 14 Sep 2018 18:59:16 +0200 Subject: [PATCH 03/86] Peer connection: let users specify a custom init message --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 65 ++++++++++--------- .../fr/acinq/eclair/io/Switchboard.scala | 4 +- 2 files changed, 35 insertions(+), 34 deletions(-) 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 cdda656a73..dbcc5b10ea 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 @@ -55,12 +55,12 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } when(DISCONNECTED) { - case Event(Peer.Connect(NodeURI(_, address)), _) => + case Event(Peer.Connect(NodeURI(_, address), init), d: DisconnectedData) => // even if we are in a reconnection loop, we immediately process explicit connection requests context.actorOf(Client.props(nodeParams, authenticator, new InetSocketAddress(address.getHost, address.getPort), remoteNodeId, origin_opt = Some(sender()))) - stay + stay using d.copy(localInit = init) - case Event(Reconnect, d@DisconnectedData(address_opt, channels, attempts)) => + case Event(Reconnect, d@DisconnectedData(address_opt, channels, attempts, _)) => address_opt match { case None => stay // no-op (this peer didn't initiate the connection and doesn't have the ip of the counterparty) case _ if channels.isEmpty => stay // no-op (no more channels with this peer) @@ -71,26 +71,27 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay using d.copy(attempts = attempts + 1) } - case Event(Authenticator.Authenticated(_, transport, remoteNodeId, address, outgoing, origin_opt), DisconnectedData(_, channels, _)) => + case Event(Authenticator.Authenticated(_, transport, remoteNodeId, address, outgoing, origin_opt), DisconnectedData(_, channels, _, init)) => log.debug(s"got authenticated connection to $remoteNodeId@${address.getHostString}:${address.getPort}") transport ! Listener(self) context watch transport - transport ! wire.Init(globalFeatures = nodeParams.globalFeatures, localFeatures = nodeParams.localFeatures) + val localInit = init.getOrElse(wire.Init(globalFeatures = nodeParams.globalFeatures, localFeatures = nodeParams.localFeatures)) + transport ! localInit // we store the ip upon successful outgoing connection, keeping only the most recent one if (outgoing) { nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, address) } - goto(INITIALIZING) using InitializingData(if (outgoing) Some(address) else None, transport, channels, origin_opt) + goto(INITIALIZING) using InitializingData(if (outgoing) Some(address) else None, transport, channels, origin_opt, localInit) - case Event(Terminated(actor), d@DisconnectedData(_, channels, _)) if channels.exists(_._2 == actor) => + case Event(Terminated(actor), d@DisconnectedData(_, channels, _, _)) if channels.exists(_._2 == actor) => val h = channels.filter(_._2 == actor).map(_._1) log.info(s"channel closed: channelId=${h.mkString("/")}") stay using d.copy(channels = channels -- h) } when(INITIALIZING) { - case Event(remoteInit: wire.Init, InitializingData(address_opt, transport, channels, origin_opt)) => + case Event(remoteInit: wire.Init, InitializingData(address_opt, transport, channels, origin_opt, localInit)) => transport ! TransportHandler.ReadAck(remoteInit) val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) @@ -123,7 +124,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor // let's bring existing/requested channels online channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(transport)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) - goto(CONNECTED) using ConnectedData(address_opt, transport, remoteInit, channels.map { case (k: ChannelId, v) => (k, v) }) + goto(CONNECTED) using ConnectedData(address_opt, transport, remoteInit, channels.map { case (k: ChannelId, v) => (k, v) }, localInit = localInit) } else { log.warning(s"incompatible features, disconnecting") origin_opt.map(origin => origin ! Status.Failure(new RuntimeException("incompatible features"))) @@ -145,25 +146,25 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor context.system.scheduler.scheduleOnce(100 milliseconds, self, o) stay - case Event(Terminated(actor), InitializingData(address_opt, transport, channels, _)) if actor == transport => + case Event(Terminated(actor), InitializingData(address_opt, transport, channels, _, _)) if actor == transport => log.warning(s"lost connection to $remoteNodeId") goto(DISCONNECTED) using DisconnectedData(address_opt, channels) - case Event(Terminated(actor), d@InitializingData(_, _, channels, _)) if channels.exists(_._2 == actor) => + case Event(Terminated(actor), d@InitializingData(_, _, channels, _, _)) if channels.exists(_._2 == actor) => val h = channels.filter(_._2 == actor).map(_._1) log.info(s"channel closed: channelId=${h.mkString("/")}") stay using d.copy(channels = channels -- h) } when(CONNECTED, stateTimeout = nodeParams.pingInterval) { - case Event(StateTimeout, ConnectedData(_, transport, _, _, _)) => + case Event(StateTimeout, ConnectedData(_, transport, _, _, _, _)) => // no need to use secure random here val pingSize = Random.nextInt(1000) val pongSize = Random.nextInt(1000) transport ! wire.Ping(pongSize, BinaryData("00" * pingSize)) stay - case Event(ping@wire.Ping(pongLength, _), ConnectedData(_, transport, _, _, _)) => + case Event(ping@wire.Ping(pongLength, _), ConnectedData(_, transport, _, _, _, _)) => transport ! TransportHandler.ReadAck(ping) // TODO: (optional) check against the expected data size tat we requested when we sent ping messages if (pongLength > 0) { @@ -171,20 +172,20 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } stay - case Event(pong@wire.Pong(data), ConnectedData(_, transport, _, _, _)) => + case Event(pong@wire.Pong(data), ConnectedData(_, transport, _, _, _, _)) => transport ! TransportHandler.ReadAck(pong) // TODO: compute latency for remote peer ? log.debug(s"received pong with ${data.length} bytes") stay - case Event(err@wire.Error(channelId, reason), ConnectedData(_, transport, _, channels, _)) if channelId == CHANNELID_ZERO => + case Event(err@wire.Error(channelId, reason), ConnectedData(_, transport, _, channels, _, _)) if channelId == CHANNELID_ZERO => transport ! TransportHandler.ReadAck(err) log.error(s"connection-level error, failing all channels! reason=${new String(reason)}") channels.values.toSet[ActorRef].foreach(_ forward err) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) transport ! PoisonPill stay - case Event(err: wire.Error, ConnectedData(_, transport, _, channels, _)) => + case Event(err: wire.Error, ConnectedData(_, transport, _, channels, _, _)) => transport ! TransportHandler.ReadAck(err) // error messages are a bit special because they can contain either temporaryChannelId or channelId (see BOLT 1) channels.get(FinalChannelId(err.channelId)).orElse(channels.get(TemporaryChannelId(err.channelId))) match { @@ -193,7 +194,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } stay - case Event(c: Peer.OpenChannel, d@ConnectedData(_, transport, remoteInit, channels, _)) => + case Event(c: Peer.OpenChannel, d@ConnectedData(_, transport, remoteInit, channels, _, _)) => log.info(s"requesting a new channel to $remoteNodeId with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt}") val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis.toLong, origin_opt = Some(sender)) val temporaryChannelId = randomBytes(32) @@ -202,7 +203,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, transport, remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel)) - case Event(msg: wire.OpenChannel, d@ConnectedData(_, transport, remoteInit, channels, _)) => + case Event(msg: wire.OpenChannel, d@ConnectedData(_, transport, remoteInit, channels, _, _)) => transport ! TransportHandler.ReadAck(msg) channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => @@ -217,7 +218,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay } - case Event(msg: wire.HasChannelId, ConnectedData(_, transport, _, channels, _)) => + case Event(msg: wire.HasChannelId, ConnectedData(_, transport, _, channels, _, _)) => transport ! TransportHandler.ReadAck(msg) channels.get(FinalChannelId(msg.channelId)) match { case Some(channel) => channel forward msg @@ -225,7 +226,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } stay - case Event(msg: wire.HasTemporaryChannelId, ConnectedData(_, transport, _, channels, _)) => + case Event(msg: wire.HasTemporaryChannelId, ConnectedData(_, transport, _, channels, _, _)) => transport ! TransportHandler.ReadAck(msg) channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case Some(channel) => channel forward msg @@ -233,13 +234,13 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } stay - case Event(ChannelIdAssigned(channel, _, temporaryChannelId, channelId), d@ConnectedData(_, _, _, channels, _)) if channels.contains(TemporaryChannelId(temporaryChannelId)) => + case Event(ChannelIdAssigned(channel, _, temporaryChannelId, channelId), d@ConnectedData(_, _, _, channels, _, _)) if channels.contains(TemporaryChannelId(temporaryChannelId)) => log.info(s"channel id switch: previousId=$temporaryChannelId nextId=$channelId") // NB: we keep the temporary channel id because the switch is not always acknowledged at this point (see https://github.com/lightningnetwork/lightning-rfc/pull/151) // we won't clean it up, but we won't remember the temporary id on channel termination stay using d.copy(channels = channels + (FinalChannelId(channelId) -> channel)) - case Event(RoutingState(channels, updates, nodes), ConnectedData(_, transport, _, _, _)) => + case Event(RoutingState(channels, updates, nodes), ConnectedData(_, transport, _, _, _, _)) => // let's send the messages def send(announcements: Iterable[_ <: LightningMessage]) = announcements.foldLeft(0) { case (c, ann) => @@ -254,7 +255,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor log.info(s"sent all announcements to {}: channels={} updates={} nodes={}", remoteNodeId, channelsSent, updatesSent, nodesSent) stay - case Event(rebroadcast: Rebroadcast, ConnectedData(_, transport, _, _, maybeGossipTimestampFilter)) => + case Event(rebroadcast: Rebroadcast, ConnectedData(_, transport, _, _, maybeGossipTimestampFilter, _)) => val (channels1, updates1, nodes1) = Peer.filterGossipMessages(rebroadcast, self, maybeGossipTimestampFilter) /** @@ -293,21 +294,21 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor router forward PeerRoutingMessage(remoteNodeId, msg) stay - case Event(readAck: TransportHandler.ReadAck, ConnectedData(_, transport, _, _, _)) => + case Event(readAck: TransportHandler.ReadAck, ConnectedData(_, transport, _, _, _, _)) => // we just forward acks from router to transport transport forward readAck stay - case Event(Disconnect, ConnectedData(_, transport, _, _, _)) => + case Event(Disconnect, ConnectedData(_, transport, _, _, _, _)) => transport ! PoisonPill stay - case Event(Terminated(actor), ConnectedData(address_opt, transport, _, channels, _)) if actor == transport => + case Event(Terminated(actor), ConnectedData(address_opt, transport, _, channels, _, _)) if actor == transport => log.info(s"lost connection to $remoteNodeId") channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) goto(DISCONNECTED) using DisconnectedData(address_opt, channels.collect { case (k: FinalChannelId, v) => (k, v) }) - case Event(Terminated(actor), d@ConnectedData(_, transport, _, channels, _)) if channels.values.toSet.contains(actor) => + case Event(Terminated(actor), d@ConnectedData(_, transport, _, channels, _, _)) if channels.values.toSet.contains(actor) => // we will have at most 2 ids: a TemporaryChannelId and a FinalChannelId val channelIds = channels.filter(_._2 == actor).keys log.info(s"channel closed: channelId=${channelIds.mkString("/")}") @@ -317,7 +318,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } stay using d.copy(channels = channels -- channelIds) - case Event(h: Authenticator.Authenticated, ConnectedData(address_opt, oldTransport, _, channels, _)) => + case Event(h: Authenticator.Authenticated, ConnectedData(address_opt, oldTransport, _, channels, _, _)) => log.info(s"got new transport while already connected, switching to new transport") context unwatch oldTransport oldTransport ! PoisonPill @@ -394,9 +395,9 @@ object Peer { def channels: Map[_ <: ChannelId, ActorRef] // will be overridden by Map[FinalChannelId, ActorRef] or Map[ChannelId, ActorRef] } case class Nothing() extends Data { override def address_opt = None; override def channels = Map.empty } - case class DisconnectedData(address_opt: Option[InetSocketAddress], channels: Map[FinalChannelId, ActorRef], attempts: Int = 0) extends Data - case class InitializingData(address_opt: Option[InetSocketAddress], transport: ActorRef, channels: Map[FinalChannelId, ActorRef], origin_opt: Option[ActorRef]) extends Data - case class ConnectedData(address_opt: Option[InetSocketAddress], transport: ActorRef, remoteInit: wire.Init, channels: Map[ChannelId, ActorRef], gossipTimestampFilter: Option[GossipTimestampFilter] = None) extends Data + case class DisconnectedData(address_opt: Option[InetSocketAddress], channels: Map[FinalChannelId, ActorRef], attempts: Int = 0, localInit: Option[wire.Init] = None) extends Data + case class InitializingData(address_opt: Option[InetSocketAddress], transport: ActorRef, channels: Map[FinalChannelId, ActorRef], origin_opt: Option[ActorRef], localInit: wire.Init) extends Data + case class ConnectedData(address_opt: Option[InetSocketAddress], transport: ActorRef, remoteInit: wire.Init, channels: Map[ChannelId, ActorRef], gossipTimestampFilter: Option[GossipTimestampFilter] = None, localInit: wire.Init) extends Data sealed trait State case object INSTANTIATING extends State @@ -405,7 +406,7 @@ object Peer { case object CONNECTED extends State case class Init(previousKnownAddress: Option[InetSocketAddress], storedChannels: Set[HasCommitments]) - case class Connect(uri: NodeURI) + case class Connect(uri: NodeURI, init: Option[wire.Init] = None) case object Reconnect case object Disconnect case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, fundingTxFeeratePerKw_opt: Option[Long], channelFlags: Option[Byte]) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index cb14ac442b..325db5bb04 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -53,10 +53,10 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto def main(peers: Map[PublicKey, ActorRef]): Receive = { - case Peer.Connect(NodeURI(publicKey, _)) if publicKey == nodeParams.nodeId => + case Peer.Connect(NodeURI(publicKey, _), _) if publicKey == nodeParams.nodeId => sender ! Status.Failure(new RuntimeException("cannot open connection with oneself")) - case c@Peer.Connect(NodeURI(remoteNodeId, _)) => + case c@Peer.Connect(NodeURI(remoteNodeId, _), init) => // we create a peer if it doesn't exist val peer = createOrGetPeer(peers, remoteNodeId, previousKnownAddress = None, offlineChannels = Set.empty) peer forward c From 6cedaeb00ae828ef7def4f5cd2cd33fbc0a86927 Mon Sep 17 00:00:00 2001 From: sstone Date: Sun, 16 Sep 2018 17:23:34 +0200 Subject: [PATCH 04/86] Correctly handle multiple channel_range_replies The scheme we use to keep tracks of channel queries with each peer would forget about missing data when several channel_range_replies are sent back for a single channel_range_queries. --- .../eclair/router/ChannelRangeQueries.scala | 6 +- .../scala/fr/acinq/eclair/router/Router.scala | 35 ++++++++--- .../fr/acinq/eclair/router/RouterSpec.scala | 11 +++- .../acinq/eclair/router/RoutingSyncSpec.scala | 60 +++++++++++++------ 4 files changed, 81 insertions(+), 31 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala index 6a662e049a..d80a40a890 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala @@ -23,16 +23,16 @@ object ChannelRangeQueries { * @param shortChannelIds must be sorted beforehand * @return a sequence of encoded short channel ids */ - def encodeShortChannelIds(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false): List[ShortChannelIdsBlock] = { + def encodeShortChannelIds(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false, maxCount: Option[Int] = None): List[ShortChannelIdsBlock] = { if (shortChannelIds.isEmpty) { // special case: reply with an "empty" block List(ShortChannelIdsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) } else { // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { + val count = maxCount.getOrElse(format match { case UNCOMPRESSED_FORMAT => 7000 case ZLIB_FORMAT => 12000 // TODO: do something less simplistic... - } + }) shortChannelIds.grouped(count).map(ids => { val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight 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 77cbcb2e65..e8571a74ac 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 @@ -21,7 +21,7 @@ import java.io.StringWriter import akka.actor.{ActorRef, Props, Status} import akka.event.Logging.MDC import akka.pattern.pipe -import fr.acinq.bitcoin.BinaryData +import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} import fr.acinq.eclair._ @@ -402,8 +402,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct .recover { case t => sender ! Status.Failure(t) } stay - case Event(SendChannelQuery(remoteNodeId, remote), _) => + case Event(SendChannelQuery(_, remote), _) => // ask for everything + // we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't + // have to worry about sending a new query_channel_range when another query is still in progress val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) log.info("sending query_channel_range={}", query) remote ! query @@ -471,8 +473,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // TODO: we don't compress to be compatible with old mobile apps, switch to ZLIB ASAP // Careful: when we remove GZIP support, eclair-wallet 0.3.0 will stop working i.e. channels to ACINQ nodes will not // work anymore - val blocks = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val maxCount = if (nodeParams.chainHash == Block.RegtestGenesisBlock.hash) Some(150) else None + val blocks = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT, maxCount = maxCount) log.info("sending back reply_channel_range with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + // there could be several reply_channel_range messages for a single query val replies = blocks.map(block => ReplyChannelRange(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) replies.foreach(reply => sender ! reply) stay @@ -484,9 +488,19 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct val missing: SortedSet[ShortChannelId] = theirShortChannelIds -- ourShortChannelIds log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={} useGzip={}", missing.size, format, useGzip) val d1 = if (missing.nonEmpty) { - val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - sender ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) + // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not + // send another query_short_channel_ids query if they're still processing one + d.sync.get(remoteNodeId) match { + case None => + // we don't have a pending query with this peer + val (slice, rest) = missing.splitAt(SHORTID_WINDOW) + sender ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip)) + Sync(rest, missing.size) + d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) + case Some(sync) => + // we already have a pending query with this peer, add missing ids to our "sync" state + d.copy(sync = d.sync + (remoteNodeId -> Sync(sync.missing ++ missing, sync.count + missing.size))) + } } else d context.system.eventStream.publish(syncProgress(d1)) stay using d1 @@ -512,11 +526,16 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct log.info("received reply_short_channel_ids_end={}", routingMessage) // have we more channels to ask this peer? val d1 = d.sync.get(remoteNodeId) match { - case Some(sync) if sync.missing.nonEmpty => + case Some(sync) => log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) sender ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) + val sync1 = if (rest.isEmpty) { + d.sync - remoteNodeId + } else { + d.sync + (remoteNodeId -> sync.copy(missing = rest)) + } + d.copy(sync = sync1) case _ => d } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index ac9c215a2a..5427b4b4fa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -28,8 +28,8 @@ import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router.Announcements.makeChannelUpdate import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.wire.Error -import fr.acinq.eclair.{ShortChannelId, randomKey} +import fr.acinq.eclair.wire.{Error, QueryChannelRange} +import fr.acinq.eclair.{ShortChannelId, TestConstants, randomKey} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -235,4 +235,11 @@ class RouterSpec extends BaseRouterSpec { assert(state.updates.size == 8) } + test("channel range queries") { case (router, _) => + val sender = TestProbe() + sender.send(router, SendChannelQuery(TestConstants.Alice.nodeParams.nodeId, sender.ref)) + val routinTable = ChannelRangeQueriesSpec.shortChannelIds.take(500).map(RoutingSyncSpec.makeFakeRoutingInfo) + val queryChannelRange = sender.expectMsgType[QueryChannelRange] + println(queryChannelRange) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 437202f6a3..840b880524 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -1,18 +1,18 @@ package fr.acinq.eclair.router import akka.actor.{Actor, ActorRef, Props} -import akka.testkit.TestProbe +import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction, TxOut} -import fr.acinq.eclair import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult, WatchSpentBasic} +import fr.acinq.eclair.crypto.TransportHandler.ReadAck import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement, RoutingMessage} +import fr.acinq.eclair.wire._ import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -24,16 +24,18 @@ class RoutingSyncSpec extends TestkitBaseClass { import RoutingSyncSpec._ val txid = BinaryData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + val idA = PrivateKey(BinaryData("01" *32), true).publicKey + val idB = PrivateKey(BinaryData("02" *32), true).publicKey type FixtureParam = Tuple3[ActorRef, ActorRef, ActorRef] val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(500) val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) - // A will be missing the last 1000 items + // A will be missing the last 100 items val routingInfoA = fakeRoutingInfo.dropRight(100) - // and B will be missing the first 1000 items - val routingInfoB = fakeRoutingInfo.drop(100) + // and B will be missing the first 100 items + val routingInfoB = fakeRoutingInfo.drop(200) class FakeWatcher extends Actor { def receive = { @@ -57,15 +59,7 @@ class RoutingSyncSpec extends TestkitBaseClass { paramsA.networkDb.addNode(n1) paramsA.networkDb.addNode(n2) } - val probe = TestProbe() - val switchboard = system.actorOf(Props(new Actor { - override def receive: Receive = { - case msg => probe.ref forward msg - } - }), "switchboard") - val routerA = system.actorOf(Props(new Router(paramsA, watcherA)), "routerA") - val idA = PrivateKey(BinaryData("01" *32), true).publicKey val watcherB = system.actorOf(Props(new FakeWatcher())) val paramsB = Bob.nodeParams @@ -78,9 +72,8 @@ class RoutingSyncSpec extends TestkitBaseClass { paramsB.networkDb.addNode(n2) } val routerB = system.actorOf(Props(new Router(paramsB, watcherB)), "routerB") - val idB = PrivateKey(BinaryData("02" *32), true).publicKey - val pipe = system.actorOf(Props(new RoutingSyncSpec.Pipe(routerA, idA, routerB, idA))) + val pipe = system.actorOf(Props(new RoutingSyncSpec.Pipe(routerA, idA, routerB, idB))) val sender = TestProbe() awaitCond({ sender.send(routerA, 'channels) @@ -96,8 +89,8 @@ class RoutingSyncSpec extends TestkitBaseClass { Globals.blockCount.set(shortChannelIds.map(id => ShortChannelId.coordinates(id).blockHeight).max) val sender = TestProbe() - routerA ! SendChannelQuery(Alice.nodeParams.nodeId, pipe) - routerB ! SendChannelQuery(Bob.nodeParams.nodeId, pipe) + routerA ! SendChannelQuery(idB, pipe) + routerB ! SendChannelQuery(idA, pipe) awaitCond({ sender.send(routerA, 'channels) @@ -105,9 +98,40 @@ class RoutingSyncSpec extends TestkitBaseClass { sender.send(routerB, 'channels) val channelsB = sender.expectMsgType[Iterable[ChannelAnnouncement]] channelsA.toSet == channelsB.toSet + channelsA.size == fakeRoutingInfo.size }, max = 30 seconds) } } + + test("handle split range replies") { + case (routerA, _, _) => { + Globals.blockCount.set(shortChannelIds.map(id => ShortChannelId.coordinates(id).blockHeight).max) + + val sender = TestProbe() + sender.ignoreMsg { + case ReadAck(_) => true + } + val routerB = TestFSMRef(new Router(Bob.nodeParams, TestProbe().ref), "routerBB") + routerB ! SendChannelQuery(Alice.nodeParams.nodeId, sender.ref) + val query = sender.expectMsgType[QueryChannelRange] + sender.expectMsgType[GossipTimestampFilter] + + sender.send(routerA, PeerRoutingMessage(idB, query)) + val reply1 = sender.expectMsgType[ReplyChannelRange] + val reply2 = sender.expectMsgType[ReplyChannelRange] + + + // tell routerB it's missing 150 channels + sender.send(routerB, PeerRoutingMessage(idA, reply1)) + // now routerB thinks it's missing 150 channels + awaitCond(routerB.stateData.sync(idA).count == 150, 3 seconds) + + // tell routerB it's missing another 150 channels + sender.send(routerB, PeerRoutingMessage(idA, reply2)) + awaitCond(routerB.stateData.sync(idA).count == 300, 3 seconds) + } + } + } object RoutingSyncSpec { From b5f53ce67e4f6d8b654a4f4f508345269700837a Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 17 Sep 2018 12:52:54 +0200 Subject: [PATCH 05/86] RoutingSync: remove peer entry properly * Remove peer entry on our sync map only when we've received a `reply_short_channel_ids_end` message. * Make routing sync test more explicit --- .../eclair/router/ChannelRangeQueries.scala | 8 +- .../scala/fr/acinq/eclair/router/Router.scala | 20 +- .../fr/acinq/eclair/router/RouterSpec.scala | 16 +- .../acinq/eclair/router/RoutingSyncSpec.scala | 190 ++++++------------ 4 files changed, 82 insertions(+), 152 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala index d80a40a890..4b9d87625d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala @@ -21,18 +21,18 @@ object ChannelRangeQueries { * Compressed a sequence of *sorted* short channel id. * * @param shortChannelIds must be sorted beforehand - * @return a sequence of encoded short channel ids + * @return a sequence of short channel id blocks */ - def encodeShortChannelIds(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false, maxCount: Option[Int] = None): List[ShortChannelIdsBlock] = { + def encodeShortChannelIds(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false): List[ShortChannelIdsBlock] = { if (shortChannelIds.isEmpty) { // special case: reply with an "empty" block List(ShortChannelIdsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) } else { // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = maxCount.getOrElse(format match { + val count = format match { case UNCOMPRESSED_FORMAT => 7000 case ZLIB_FORMAT => 12000 // TODO: do something less simplistic... - }) + } shortChannelIds.grouped(count).map(ids => { val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight 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 e8571a74ac..8e5a43eb74 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 @@ -74,7 +74,8 @@ case class Data(nodes: Map[PublicKey, NodeAnnouncement], privateUpdates: Map[ChannelDesc, ChannelUpdate], excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure graph: DirectedWeightedPseudograph[PublicKey, DescEdge], - sync: Map[PublicKey, Sync] + sync: Map[PublicKey, Sync] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query + // for which we have not yet received an 'end' message ) sealed trait State @@ -473,8 +474,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // TODO: we don't compress to be compatible with old mobile apps, switch to ZLIB ASAP // Careful: when we remove GZIP support, eclair-wallet 0.3.0 will stop working i.e. channels to ACINQ nodes will not // work anymore - val maxCount = if (nodeParams.chainHash == Block.RegtestGenesisBlock.hash) Some(150) else None - val blocks = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT, maxCount = maxCount) + val blocks = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) log.info("sending back reply_channel_range with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) // there could be several reply_channel_range messages for a single query val replies = blocks.map(block => ReplyChannelRange(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) @@ -495,7 +495,6 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // we don't have a pending query with this peer val (slice, rest) = missing.splitAt(SHORTID_WINDOW) sender ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip)) - Sync(rest, missing.size) d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state @@ -526,16 +525,15 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct log.info("received reply_short_channel_ids_end={}", routingMessage) // have we more channels to ask this peer? val d1 = d.sync.get(remoteNodeId) match { - case Some(sync) => + case Some(sync) if sync.missing.nonEmpty => log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) sender ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - val sync1 = if (rest.isEmpty) { - d.sync - remoteNodeId - } else { - d.sync + (remoteNodeId -> sync.copy(missing = rest)) - } - d.copy(sync = sync1) + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) + case Some(sync) if sync.missing.isEmpty => + // we received reply_short_channel_ids_end for our last query aand have not sent another one, we can now remove + // the remote peer from our map + d.copy(sync = d.sync - remoteNodeId) case _ => d } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 5427b4b4fa..685f15aa67 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -18,9 +18,8 @@ package fr.acinq.eclair.router import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.{Block, Satoshi, Transaction, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT import fr.acinq.eclair.crypto.TransportHandler @@ -28,8 +27,8 @@ import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router.Announcements.makeChannelUpdate import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.wire.{Error, QueryChannelRange} -import fr.acinq.eclair.{ShortChannelId, TestConstants, randomKey} +import fr.acinq.eclair.wire.Error +import fr.acinq.eclair.{ShortChannelId, randomKey} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -40,7 +39,6 @@ import scala.concurrent.duration._ */ @RunWith(classOf[JUnitRunner]) class RouterSpec extends BaseRouterSpec { - import BaseRouterSpec._ test("properly announce valid new channels and ignore invalid ones") { case (router, watcher) => val eventListener = TestProbe() @@ -234,12 +232,4 @@ class RouterSpec extends BaseRouterSpec { assert(state.nodes.size == 6) assert(state.updates.size == 8) } - - test("channel range queries") { case (router, _) => - val sender = TestProbe() - sender.send(router, SendChannelQuery(TestConstants.Alice.nodeParams.nodeId, sender.ref)) - val routinTable = ChannelRangeQueriesSpec.shortChannelIds.take(500).map(RoutingSyncSpec.makeFakeRoutingInfo) - val queryChannelRange = sender.expectMsgType[QueryChannelRange] - println(queryChannelRange) - } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 840b880524..858613bf3b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -1,139 +1,88 @@ package fr.acinq.eclair.router -import akka.actor.{Actor, ActorRef, Props} -import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction, TxOut} +import akka.actor.ActorSystem +import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import fr.acinq.bitcoin.Block import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult, WatchSpentBasic} -import fr.acinq.eclair.crypto.TransportHandler.ReadAck +import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement -import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ import org.junit.runner.RunWith +import org.scalatest.FunSuiteLike import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ @RunWith(classOf[JUnitRunner]) -class RoutingSyncSpec extends TestkitBaseClass { +class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { + import RoutingSyncSpec.makeFakeRoutingInfo - import RoutingSyncSpec._ - - val txid = BinaryData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - val idA = PrivateKey(BinaryData("01" *32), true).publicKey - val idB = PrivateKey(BinaryData("02" *32), true).publicKey - - type FixtureParam = Tuple3[ActorRef, ActorRef, ActorRef] - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(500) - - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) - // A will be missing the last 100 items - val routingInfoA = fakeRoutingInfo.dropRight(100) - // and B will be missing the first 100 items - val routingInfoB = fakeRoutingInfo.drop(200) - - class FakeWatcher extends Actor { - def receive = { - case _: WatchSpentBasic => () - case ValidateRequest(ann) => - val txOut = TxOut(Satoshi(1000000), Script.pay2wsh(Scripts.multiSig2of2(ann.bitcoinKey1, ann.bitcoinKey2))) - val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(ann.shortChannelId) - sender ! ValidateResult(ann, Some(Transaction(version = 0, txIn = Nil, txOut = List.fill(outputIndex + 1)(txOut), lockTime = 0)), true, None) - case unexpected => println(s"unexpected : $unexpected") - } - } - - override def withFixture(test: OneArgTest) = { - val watcherA = system.actorOf(Props(new FakeWatcher())) - val paramsA = Alice.nodeParams - routingInfoA.map { - case (a, u1, u2, n1, n2) => - paramsA.networkDb.addChannel(a, txid, Satoshi(100000)) - paramsA.networkDb.addChannelUpdate(u1) - paramsA.networkDb.addChannelUpdate(u2) - paramsA.networkDb.addNode(n1) - paramsA.networkDb.addNode(n2) - } - val routerA = system.actorOf(Props(new Router(paramsA, watcherA)), "routerA") - - val watcherB = system.actorOf(Props(new FakeWatcher())) - val paramsB = Bob.nodeParams - routingInfoB.map { - case (a, u1, u2, n1, n2) => - paramsB.networkDb.addChannel(a, txid, Satoshi(100000)) - paramsB.networkDb.addChannelUpdate(u1) - paramsB.networkDb.addChannelUpdate(u2) - paramsB.networkDb.addNode(n1) - paramsB.networkDb.addNode(n2) - } - val routerB = system.actorOf(Props(new Router(paramsB, watcherB)), "routerB") - - val pipe = system.actorOf(Props(new RoutingSyncSpec.Pipe(routerA, idA, routerB, idB))) + test("handle chanel range queries") { + val params = TestConstants.Alice.nodeParams + val router = TestFSMRef(new Router(params, TestProbe().ref)) val sender = TestProbe() - awaitCond({ - sender.send(routerA, 'channels) - val channelsA = sender.expectMsgType[Iterable[ChannelAnnouncement]] - channelsA.size == routingInfoA.size - }, max = 30 seconds) - - test((routerA, routerB, pipe)) + sender.ignoreMsg { case _: TransportHandler.ReadAck => true } + val remoteNodeId = TestConstants.Bob.nodeParams.nodeId + + // ask router to send a channel range query + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) + val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] + sender.expectMsgType[GossipTimestampFilter] + + + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) + val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap + + // split our anwser in 3 blocks + val List(block1) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block2) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block3) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + + // send first block + sender.send(router, PeerRoutingMessage(remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) + // router should ask for our first block of ids + val QueryShortChannelIds(_, data1) = sender.expectMsgType[QueryShortChannelIds] + val (_, shortChannelIds1, false) = ChannelRangeQueries.decodeShortChannelIds(data1) + assert(shortChannelIds1 == shortChannelIds.take(100)) + + // send second block + sender.send(router, PeerRoutingMessage(remoteNodeId, ReplyChannelRange(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIds))) + + // router should not ask for more ids, it already has a pending query ! + sender.expectNoMsg(1 second) + + // send the first 50 items + shortChannelIds1.take(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) + sender.send(router, PeerRoutingMessage(remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(remoteNodeId, cu2)) + }) + sender.expectNoMsg(1 second) + + // send the last 50 items + shortChannelIds1.drop(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) + sender.send(router, PeerRoutingMessage(remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(remoteNodeId, cu2)) + }) + sender.expectNoMsg(1 second) + + // now send our ReplyShortChannelIdsEnd message + sender.send(router, PeerRoutingMessage(remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) + + // router should ask for our second block of ids + val QueryShortChannelIds(_, data2) = sender.expectMsgType[QueryShortChannelIds] + val (_, shortChannelIds2, false) = ChannelRangeQueries.decodeShortChannelIds(data2) + assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) } - - test("initial sync") { - case (routerA, routerB, pipe) => { - Globals.blockCount.set(shortChannelIds.map(id => ShortChannelId.coordinates(id).blockHeight).max) - - val sender = TestProbe() - routerA ! SendChannelQuery(idB, pipe) - routerB ! SendChannelQuery(idA, pipe) - - awaitCond({ - sender.send(routerA, 'channels) - val channelsA = sender.expectMsgType[Iterable[ChannelAnnouncement]] - sender.send(routerB, 'channels) - val channelsB = sender.expectMsgType[Iterable[ChannelAnnouncement]] - channelsA.toSet == channelsB.toSet - channelsA.size == fakeRoutingInfo.size - }, max = 30 seconds) - } - } - - test("handle split range replies") { - case (routerA, _, _) => { - Globals.blockCount.set(shortChannelIds.map(id => ShortChannelId.coordinates(id).blockHeight).max) - - val sender = TestProbe() - sender.ignoreMsg { - case ReadAck(_) => true - } - val routerB = TestFSMRef(new Router(Bob.nodeParams, TestProbe().ref), "routerBB") - routerB ! SendChannelQuery(Alice.nodeParams.nodeId, sender.ref) - val query = sender.expectMsgType[QueryChannelRange] - sender.expectMsgType[GossipTimestampFilter] - - sender.send(routerA, PeerRoutingMessage(idB, query)) - val reply1 = sender.expectMsgType[ReplyChannelRange] - val reply2 = sender.expectMsgType[ReplyChannelRange] - - - // tell routerB it's missing 150 channels - sender.send(routerB, PeerRoutingMessage(idA, reply1)) - // now routerB thinks it's missing 150 channels - awaitCond(routerB.stateData.sync(idA).count == 150, 3 seconds) - - // tell routerB it's missing another 150 channels - sender.send(routerB, PeerRoutingMessage(idA, reply2)) - awaitCond(routerB.stateData.sync(idA).count == 300, 3 seconds) - } - } - } + object RoutingSyncSpec { def makeFakeRoutingInfo(shortChannelId: ShortChannelId): (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement) = { val (priv_a, priv_b, priv_funding_a, priv_funding_b) = (randomKey, randomKey, randomKey, randomKey) @@ -145,11 +94,4 @@ object RoutingSyncSpec { val nodeAnnouncement_b = makeNodeAnnouncement(priv_b, "b", Bob.nodeParams.color, List()) (channelAnn_ab, channelUpdate_ab, channelUpdate_ba, nodeAnnouncement_a, nodeAnnouncement_b) } - - class Pipe(a: ActorRef, idA: PublicKey, b: ActorRef, idB: PublicKey) extends Actor { - def receive = { - case msg: RoutingMessage if sender == a => b ! PeerRoutingMessage(idA, msg) - case msg: RoutingMessage if sender == b => a ! PeerRoutingMessage(idB, msg) - } - } } From 03c45bb1cdd4150e8a8708697c6dcb5977472e16 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 17 Sep 2018 13:11:17 +0200 Subject: [PATCH 06/86] Routing Sync: rename Sync.count to Sync.totalMissingCount --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 8e5a43eb74..ecdbd92070 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 @@ -60,7 +60,7 @@ case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterab case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) -case class Sync(missing: SortedSet[ShortChannelId], count: Int) +case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int) case class DescEdge(desc: ChannelDesc, u: ChannelUpdate) extends DefaultWeightedEdge @@ -498,7 +498,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> Sync(sync.missing ++ missing, sync.count + missing.size))) + d.copy(sync = d.sync + (remoteNodeId -> Sync(sync.missing ++ missing, sync.totalMissingCount + missing.size))) } } else d context.system.eventStream.publish(syncProgress(d1)) @@ -760,7 +760,7 @@ object Router { if (d.sync.isEmpty) { SyncProgress(1) } else { - SyncProgress(1 - d.sync.values.map(_.missing.size).sum * 1.0 / d.sync.values.map(_.count).sum) + SyncProgress(1 - d.sync.values.map(_.missing.size).sum * 1.0 / d.sync.values.map(_.totalMissingCount).sum) } /** From 0da9136536ed341fe391f12cd17bf26ecc8a103a Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 18 Sep 2018 17:34:01 +0200 Subject: [PATCH 07/86] Do not send channel queries if we don't want to sync --- .../src/main/scala/fr/acinq/eclair/io/Peer.scala | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 8525d3cdf3..fd953791d8 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 @@ -95,6 +95,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(INITIALIZING) { case Event(remoteInit: wire.Init, InitializingData(address_opt, transport, channels, origin_opt, localInit)) => transport ! TransportHandler.ReadAck(remoteInit) + val localHasInitialRoutingSync = Features.hasFeature(localInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) @@ -116,12 +117,14 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor router ! GetRoutingState } } - if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { - // if they support extended channel queries, always ask for their filter - router ! SendChannelQueryEx(remoteNodeId, transport) - } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { - // if they support channel queries, always ask for their filter - router ! SendChannelQuery(remoteNodeId, transport) + if (localHasInitialRoutingSync) { + if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { + // if they support extended channel queries, always ask for their filter + router ! SendChannelQueryEx(remoteNodeId, transport) + } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + // if they support channel queries, always ask for their filter + router ! SendChannelQuery(remoteNodeId, transport) + } } // let's bring existing/requested channels online From f7b3409bbd1e304de9207c642bea3acae9c464ed Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 19 Sep 2018 11:00:11 +0200 Subject: [PATCH 08/86] Router: clean our sync state when we (re)connect to a peer We must clean up leftovers for the previous session and start the sync process again. --- .../main/scala/fr/acinq/eclair/router/Router.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 bf63c440f6..3ee57fa1b0 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 @@ -393,7 +393,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct .recover { case t => sender ! Status.Failure(t) } stay - case Event(SendChannelQuery(_, remote), _) => + case Event(SendChannelQuery(remoteNodeId, remote), d) => // ask for everything // we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't // have to worry about sending a new query_channel_range when another query is still in progress @@ -404,9 +404,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // we also set a pass-all filter for now (we can update it later) val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) remote ! filter - stay - case Event(SendChannelQueryEx(remoteNodeId, remote), _) => + // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and + // will start a new complete sync process + stay using d.copy(sync = d.sync - remoteNodeId) + + case Event(SendChannelQueryEx(remoteNodeId, remote), d) => // ask for everything val query = QueryChannelRangeEx(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) log.info("sending query_channel_range_ex={}", query) @@ -415,7 +418,9 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // we also set a pass-all filter for now (we can update it later) val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) remote ! filter - stay + // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and + // will start a new complete sync process + stay using d.copy(sync = d.sync - remoteNodeId) // Warning: order matters here, this must be the first match for HasChainHash messages ! case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), d) if routingMessage.chainHash != nodeParams.chainHash => From e740523698b5cbed04c677c04a7959538478db6e Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 19 Sep 2018 11:37:49 +0200 Subject: [PATCH 09/86] Router: fix syncing of outdated channels --- .../main/scala/fr/acinq/eclair/router/Router.scala | 12 ------------ 1 file changed, 12 deletions(-) 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 3ee57fa1b0..1ec5f91eb5 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 @@ -569,18 +569,6 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // we already have a pending query with this peer, add outdated ids to our "sync" state d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = sync.outdated ++ outdated, totalOutdatedCount = sync.totalOutdatedCount + outdated.size))) } - // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not - // send another query_short_channel_ids query if they're still processing one - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> Sync(sync.missing ++ missing, sync.totalMissingCount + missing.size))) - } } else d context.system.eventStream.publish(syncProgress(d1)) stay using d1 From 19f0c811a88e8ee0d523c78865f4384b7fb7c2b9 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 19 Sep 2018 13:42:12 +0200 Subject: [PATCH 10/86] Routing: don't request their channel ids if we don't want a routing table dump --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 11 ++++-- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 37 ++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) 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 cd4a3d3780..18a5c2ad6c 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 @@ -94,6 +94,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(INITIALIZING) { case Event(remoteInit: wire.Init, InitializingData(address_opt, transport, channels, origin_opt)) => transport ! TransportHandler.ReadAck(remoteInit) + val localHasInitialRoutingSync = Features.hasFeature(nodeParams.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) @@ -110,9 +111,13 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor router ! GetRoutingState } } - if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { - // if they support channel queries, always ask for their filter - router ! SendChannelQuery(remoteNodeId, transport) + if (localHasInitialRoutingSync) { + // if we want a routing table dump and do not support range queries they will send it to us + // but if we do support range queries we have to ask, this mirrors the behaviour above + if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + // if they support channel queries, ask for their filter + router ! SendChannelQuery(remoteNodeId, transport) + } } // let's bring existing/requested channels online 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 b56d3d416a..2e6adc07d5 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 @@ -4,13 +4,13 @@ import java.net.InetSocketAddress import akka.actor.ActorRef import akka.testkit.TestProbe -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.ResumeAnnouncements import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast, RouteCalculationSpec} +import fr.acinq.eclair.router._ import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, randomKey, wire} import org.junit.runner.RunWith import org.scalatest.Outcome @@ -143,4 +143,37 @@ class PeerSpec extends TestkitBaseClass { } transport.expectNoMsg(1 second) // peer hasn't acknowledged the messages } + + test("send channel queries if local feature 'initial_routing_sync' is set") { probe => + val authenticator = TestProbe() + val watcher = TestProbe() + val router = TestProbe() + val relayer = TestProbe() + val connection = TestProbe() + val transport = TestProbe() + val wallet: EclairWallet = null // unused + val remoteNodeId = Bob.nodeParams.nodeId + val peer = system.actorOf(Peer.props(Alice.nodeParams.copy(localFeatures = BinaryData("8a")), remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) + probe.send(peer, Peer.Init(None, Set())) + probe.send(peer, Authenticator.Authenticated(null, transport.ref, remoteNodeId, InetSocketAddress.createUnresolved("localhost", 9735), false, None)) + probe.send(peer, wire.Init(BinaryData.empty, BinaryData("8a"))) + router.expectMsgType[SendChannelQuery] + } + + test("don't send channel queries if local feature 'initial_routing_sync' is not set") { probe => + val authenticator = TestProbe() + val watcher = TestProbe() + val router = TestProbe() + val relayer = TestProbe() + val connection = TestProbe() + val transport = TestProbe() + val wallet: EclairWallet = null // unused + val remoteNodeId = Bob.nodeParams.nodeId + // 0x82 means that we support channel range queries and data loss protection but do not want a routing table dump + val peer = system.actorOf(Peer.props(Alice.nodeParams.copy(localFeatures = BinaryData("82")), remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) + probe.send(peer, Peer.Init(None, Set())) + probe.send(peer, Authenticator.Authenticated(null, transport.ref, remoteNodeId, InetSocketAddress.createUnresolved("localhost", 9735), false, None)) + probe.send(peer, wire.Init(BinaryData.empty, BinaryData("8a"))) + router.expectNoMsg(3 second) + } } From ebcd58cde177cad9fc77916dc094a7555c4c8592 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 19 Sep 2018 13:58:18 +0200 Subject: [PATCH 11/86] Router: fix indentation --- .../scala/fr/acinq/eclair/router/Router.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 92eb9fea1a..a8816e9bf3 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 @@ -64,17 +64,17 @@ case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int) case class DescEdge(desc: ChannelDesc, u: ChannelUpdate) extends DefaultWeightedEdge case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, ChannelAnnouncement], - updates: Map[ChannelDesc, ChannelUpdate], - stash: Stash, - rebroadcast: Rebroadcast, - awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done - privateChannels: Map[ShortChannelId, PublicKey], // short_channel_id -> node_id - privateUpdates: Map[ChannelDesc, ChannelUpdate], - excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure - graph: DirectedWeightedPseudograph[PublicKey, DescEdge], - sync: Map[PublicKey, Sync] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query - // for which we have not yet received an 'end' message + channels: SortedMap[ShortChannelId, ChannelAnnouncement], + updates: Map[ChannelDesc, ChannelUpdate], + stash: Stash, + rebroadcast: Rebroadcast, + awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done + privateChannels: Map[ShortChannelId, PublicKey], // short_channel_id -> node_id + privateUpdates: Map[ChannelDesc, ChannelUpdate], + excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure + graph: DirectedWeightedPseudograph[PublicKey, DescEdge], + sync: Map[PublicKey, Sync] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query + // for which we have not yet received an 'end' message ) sealed trait State @@ -521,7 +521,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) transport ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) case Some(sync) if sync.missing.isEmpty => // we received reply_short_channel_ids_end for our last query aand have not sent another one, we can now remove // the remote peer from our map From c345b2fdcefef2584629b4818c40efbee03aad62 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 19 Sep 2018 14:10:09 +0200 Subject: [PATCH 12/86] Router: reset sync state on reconnection When we're reconnected to a peer we will start a new sync process and should reset our sync state with that peer. --- .../scala/fr/acinq/eclair/router/Router.scala | 7 +++- .../acinq/eclair/router/RoutingSyncSpec.scala | 38 +++++++++++++++++-- 2 files changed, 39 insertions(+), 6 deletions(-) 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 a8816e9bf3..b99ab813dc 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 @@ -390,7 +390,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct .recover { case t => sender ! Status.Failure(t) } stay - case Event(SendChannelQuery(_, remote), _) => + case Event(SendChannelQuery(remoteNodeId, remote), d) => // ask for everything // we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't // have to worry about sending a new query_channel_range when another query is still in progress @@ -401,7 +401,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // we also set a pass-all filter for now (we can update it later) val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) remote ! filter - stay + + // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and + // will start a new complete sync process + stay using d.copy(sync = d.sync - remoteNodeId) // Warning: order matters here, this must be the first match for HasChainHash messages ! case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), d) if routingMessage.chainHash != nodeParams.chainHash => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index a3f1091cec..7c4fdb05fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -21,6 +21,9 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { import RoutingSyncSpec.makeFakeRoutingInfo + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) + val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap + test("handle channel range queries") { val params = TestConstants.Alice.nodeParams val router = TestFSMRef(new Router(params, TestProbe().ref)) @@ -34,10 +37,6 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - // split our anwser in 3 blocks val List(block1) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) val List(block2) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) @@ -80,6 +79,37 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val (_, shortChannelIds2, false) = ChannelRangeQueries.decodeShortChannelIds(data2) assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) } + + test("reset sync state on reconnection") { + val params = TestConstants.Alice.nodeParams + val router = TestFSMRef(new Router(params, TestProbe().ref)) + val transport = TestProbe() + val sender = TestProbe() + sender.ignoreMsg { case _: TransportHandler.ReadAck => true } + val remoteNodeId = TestConstants.Bob.nodeParams.nodeId + + // ask router to send a channel range query + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) + val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] + sender.expectMsgType[GossipTimestampFilter] + + val List(block1) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + + // send first block + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) + + // router should ask for our first block of ids + val QueryShortChannelIds(_, data1) = transport.expectMsgType[QueryShortChannelIds] + // router should think that it is mssing 100 channels + val Some(sync) = router.stateData.sync.get(remoteNodeId) + assert(sync.totalMissingCount == 100) + + // simulate a re-connection + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) + sender.expectMsgType[QueryChannelRange] + sender.expectMsgType[GossipTimestampFilter] + assert(router.stateData.sync.get(remoteNodeId).isEmpty) + } } From 830f9636b77ebf5f1874a244faf55ed5bf4be688 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 19 Sep 2018 14:54:20 +0200 Subject: [PATCH 13/86] Revert "Routing: don't request their channel ids if we don't want a routing table dump" This reverts commit 19f0c811a88e8ee0d523c78865f4384b7fb7c2b9. --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 11 ++---- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 37 +------------------ 2 files changed, 5 insertions(+), 43 deletions(-) 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 18a5c2ad6c..cd4a3d3780 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 @@ -94,7 +94,6 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(INITIALIZING) { case Event(remoteInit: wire.Init, InitializingData(address_opt, transport, channels, origin_opt)) => transport ! TransportHandler.ReadAck(remoteInit) - val localHasInitialRoutingSync = Features.hasFeature(nodeParams.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) @@ -111,13 +110,9 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor router ! GetRoutingState } } - if (localHasInitialRoutingSync) { - // if we want a routing table dump and do not support range queries they will send it to us - // but if we do support range queries we have to ask, this mirrors the behaviour above - if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { - // if they support channel queries, ask for their filter - router ! SendChannelQuery(remoteNodeId, transport) - } + if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + // if they support channel queries, always ask for their filter + router ! SendChannelQuery(remoteNodeId, transport) } // let's bring existing/requested channels online 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 2e6adc07d5..b56d3d416a 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 @@ -4,13 +4,13 @@ import java.net.InetSocketAddress import akka.actor.ActorRef import akka.testkit.TestProbe -import fr.acinq.bitcoin.{BinaryData, Block} +import fr.acinq.bitcoin.Block import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.ResumeAnnouncements import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router._ +import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast, RouteCalculationSpec} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, randomKey, wire} import org.junit.runner.RunWith import org.scalatest.Outcome @@ -143,37 +143,4 @@ class PeerSpec extends TestkitBaseClass { } transport.expectNoMsg(1 second) // peer hasn't acknowledged the messages } - - test("send channel queries if local feature 'initial_routing_sync' is set") { probe => - val authenticator = TestProbe() - val watcher = TestProbe() - val router = TestProbe() - val relayer = TestProbe() - val connection = TestProbe() - val transport = TestProbe() - val wallet: EclairWallet = null // unused - val remoteNodeId = Bob.nodeParams.nodeId - val peer = system.actorOf(Peer.props(Alice.nodeParams.copy(localFeatures = BinaryData("8a")), remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) - probe.send(peer, Peer.Init(None, Set())) - probe.send(peer, Authenticator.Authenticated(null, transport.ref, remoteNodeId, InetSocketAddress.createUnresolved("localhost", 9735), false, None)) - probe.send(peer, wire.Init(BinaryData.empty, BinaryData("8a"))) - router.expectMsgType[SendChannelQuery] - } - - test("don't send channel queries if local feature 'initial_routing_sync' is not set") { probe => - val authenticator = TestProbe() - val watcher = TestProbe() - val router = TestProbe() - val relayer = TestProbe() - val connection = TestProbe() - val transport = TestProbe() - val wallet: EclairWallet = null // unused - val remoteNodeId = Bob.nodeParams.nodeId - // 0x82 means that we support channel range queries and data loss protection but do not want a routing table dump - val peer = system.actorOf(Peer.props(Alice.nodeParams.copy(localFeatures = BinaryData("82")), remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) - probe.send(peer, Peer.Init(None, Set())) - probe.send(peer, Authenticator.Authenticated(null, transport.ref, remoteNodeId, InetSocketAddress.createUnresolved("localhost", 9735), false, None)) - probe.send(peer, wire.Init(BinaryData.empty, BinaryData("8a"))) - router.expectNoMsg(3 second) - } } From da012f933e2924392a1f69a43899679bf9aa34b5 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 21 Sep 2018 18:27:49 +0200 Subject: [PATCH 14/86] add extended query flag to our log message It will tell us if sender requests channel announcements and updates or just updates --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 794489d17b..a708164705 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 @@ -590,8 +590,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsEx(chainHash, flag, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (_, shortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) - log.info("received query_short_channel_ids_ex for {} channel announcements, useGzip={}", shortChannelIds.size, useGzip) + val (_, shortChannelIds, _) = ChannelRangeQueries.decodeShortChannelIds(data) + log.info("received query_short_channel_ids_ex for {} channel announcements, flag={}", shortChannelIds.size, flag) shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) From eb4482358cc390183e25e4b66a8d16713907b63e Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 26 Oct 2018 16:08:26 +0200 Subject: [PATCH 15/86] Router: implement new extended queries - reply_channel_range include a list of channel id + both channel update timestamp - query_short_channl_ids include a list of channel id + flag, where flag specifies what the receiver should send back (a combination of channel annoucement, channel updated #1 and channel update #2) --- .../eclair/router/ChannelRangeQueries.scala | 112 ++------- .../scala/fr/acinq/eclair/router/Router.scala | 175 ++++++++++++-- ... => ShortChannelIdAndTimestampBlock.scala} | 42 ++-- .../ShortChannelIdAndTimestampsBlock.scala | 226 ++++++++++++++++++ .../eclair/router/ShortChannelIdsBlock.scala | 99 ++++++++ .../eclair/wire/LightningMessageCodecs.scala | 38 ++- .../eclair/wire/LightningMessageTypes.scala | 15 ++ .../scala/fr/acinq/eclair/io/PeerSpec.scala | 4 +- .../router/ChannelRangeQueriesExSpec.scala | 8 +- .../router/ChannelRangeQueriesSpec.scala | 61 ++++- .../fr/acinq/eclair/router/RouterSpec.scala | 2 +- .../eclair/router/RouterSyncEx2Spec.scala | 84 +++++++ .../eclair/router/RoutingSyncExSpec.scala | 10 +- .../acinq/eclair/router/RoutingSyncSpec.scala | 12 +- 14 files changed, 714 insertions(+), 174 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/router/{ChannelRangeQueriesEx.scala => ShortChannelIdAndTimestampBlock.scala} (64%) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala index 6436d9036c..b9c8a63a53 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala @@ -1,111 +1,31 @@ package fr.acinq.eclair.router -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} -import java.nio.ByteOrder -import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream} - -import fr.acinq.bitcoin.{BinaryData, Protocol} -import fr.acinq.eclair.ShortChannelId +import java.io.InputStream import scala.annotation.tailrec -import scala.collection.SortedSet object ChannelRangeQueries { val UNCOMPRESSED_FORMAT = 0.toByte val ZLIB_FORMAT = 1.toByte + val INCLUDE_CHANNEL_UPDATE_1 = 1.toByte + val INCLUDE_CHANNEL_UPDATE_2 = 2.toByte + val INCLUDE_ANNOUNCEMENT = 4.toByte - case class ShortChannelIdsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIds: BinaryData) - - /** - * Compressed a sequence of *sorted* short channel id. - * - * @param shortChannelIds must be sorted beforehand - * @return a sequence of short channel id blocks - */ - def encodeShortChannelIds(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false): List[ShortChannelIdsBlock] = { - if (shortChannelIds.isEmpty) { - // special case: reply with an "empty" block - List(ShortChannelIdsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) - } else { - // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { - case UNCOMPRESSED_FORMAT => 7000 - case ZLIB_FORMAT => 12000 // TODO: do something less simplistic... - } - shortChannelIds.grouped(count).map(ids => { - val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { - val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight - val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 - (firstBlock, numBlocks) - } - val encoded = encodeShortChannelIdsSingle(ids, format, useGzip) - ShortChannelIdsBlock(firstBlock, numBlocks, encoded) - }).toList - } - } - - def encodeShortChannelIdsSingle(shortChannelIds: Iterable[ShortChannelId], format: Byte, useGzip: Boolean): BinaryData = { - val bos = new ByteArrayOutputStream() - bos.write(format) - val out = format match { - case UNCOMPRESSED_FORMAT => bos - case ZLIB_FORMAT => if (useGzip) new GZIPOutputStream(bos) else new DeflaterOutputStream(bos) - } - shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN)) - out.close() - bos.toByteArray - } - - /** - * Decompress a zipped sequence of sorted short channel ids. - * - * @param data - * @return a sorted set of short channel ids - */ - def decodeShortChannelIds(data: BinaryData): (Byte, SortedSet[ShortChannelId], Boolean) = { - val format = data.head - if (data.tail.isEmpty) (format, SortedSet.empty[ShortChannelId], false) else { - val buffer = new Array[Byte](8) + def includeAnnoucement(flag: Byte) = (flag & INCLUDE_ANNOUNCEMENT) != 0 - // read 8 bytes from input - // zipped input stream often returns less bytes than what you want to read - @tailrec - def read8(input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, 8 - offset) match { - case len if len <= 0 => len - case 8 => 8 - case len if offset + len == 8 => 8 - case len => read8(input, offset + len) - } + def includeUpdate1(flag: Byte) = (flag & INCLUDE_CHANNEL_UPDATE_1) != 0 - // read until there's nothing left - @tailrec - def loop(input: InputStream, acc: SortedSet[ShortChannelId]): SortedSet[ShortChannelId] = { - val check = read8(input) - if (check <= 0) acc else loop(input, acc + ShortChannelId(Protocol.uint64(buffer, ByteOrder.BIG_ENDIAN))) - } + def includeUpdate2(flag: Byte) = (flag & INCLUDE_CHANNEL_UPDATE_2) != 0 - def readAll(useGzip: Boolean) = { - val bis = new ByteArrayInputStream(data.tail.toArray) - val input = format match { - case UNCOMPRESSED_FORMAT => bis - case ZLIB_FORMAT if useGzip => new GZIPInputStream(bis) - case ZLIB_FORMAT => new InflaterInputStream(bis) - } - try { - (format, loop(input, SortedSet.empty[ShortChannelId]), useGzip) - } - finally { - input.close() - } - } - - try { - readAll(useGzip = false) - } - catch { - case _: Throwable if format == ZLIB_FORMAT => readAll(useGzip = true) - } - } + // read bytes from an input stream + // zipped input stream often returns less bytes than what you want to read + @tailrec + def readBytes(buffer: Array[Byte], input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, buffer.length - offset) match { + case len if len <= 0 => len + case len if len == buffer.length => buffer.length + case len if offset + len == buffer.length => buffer.length + case len => readBytes(buffer, input, offset + len) } + } 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 9592aa6d58..b740a5b442 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 @@ -24,11 +24,11 @@ import akka.pattern.pipe import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.eclair._ +import fr.acinq.eclair.{router, _} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.{ChannelClosed, NonexistingChannel, InvalidSignature, PeerRoutingMessage} +import fr.acinq.eclair.io.Peer.{ChannelClosed, InvalidSignature, NonexistingChannel, PeerRoutingMessage} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ @@ -57,12 +57,22 @@ case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a Tempo case class LiftChannelExclusion(desc: ChannelDesc) case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) case class SendChannelQueryEx(remoteNodeId: PublicKey, to: ActorRef) +case class SendChannelQueryEx2(remoteNodeId: PublicKey, to: ActorRef) case object GetRoutingState case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) -case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int, outdated: SortedSet[ShortChannelId] = SortedSet.empty[ShortChannelId], totalOutdatedCount: Int = 0) +case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int, + outdated: SortedSet[ShortChannelId] = SortedSet.empty[ShortChannelId], totalOutdatedCount: Int = 0, + outdatedEx :SortedSet[ShortChannelIdAndFlag] = SortedSet.empty[ShortChannelIdAndFlag], totalOutdatedExCount: Int = 0) { + // returns a sync progress indicator (1 means fully synced) + def progress: Double = { + val inflight = (missing.size + outdated.size + outdatedEx.size) * 1.0 + val total = totalMissingCount + totalOutdatedCount + totalOutdatedExCount + if (total == 0) 1.0 else inflight / total + } +} case class DescEdge(desc: ChannelDesc, u: ChannelUpdate) extends DefaultWeightedEdge @@ -421,6 +431,18 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) + case Event(SendChannelQueryEx2(remoteNodeId, remote), d) => + // ask for everything + val query = QueryChannelRangeEx2(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + log.info("sending query_channel_range_ex2={}", query) + remote ! query + // we also set a pass-all filter for now (we can update it later) + val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) + remote ! filter + // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and + // will start a new complete sync process + stay using d.copy(sync = d.sync - remoteNodeId) + // Warning: order matters here, this must be the first match for HasChainHash messages ! case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), d) if routingMessage.chainHash != nodeParams.chainHash => sender ! TransportHandler.ReadAck(routingMessage) @@ -484,7 +506,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // TODO: we don't compress to be compatible with old mobile apps, switch to ZLIB ASAP // Careful: when we remove GZIP support, eclair-wallet 0.3.0 will stop working i.e. channels to ACINQ nodes will not // work anymore - val blocks = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val blocks = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) log.info("sending back reply_channel_range with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) // there could be several reply_channel_range messages for a single query val replies = blocks.map(block => ReplyChannelRange(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) @@ -496,15 +518,26 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct log.info("received query_channel_range_ex={}", routingMessage) // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) - val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamp(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val blocks = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamp(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) log.info("sending back reply_channel_range_ex with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) val replies = blocks.map(block => ReplyChannelRangeEx(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) replies.foreach(reply => transport ! reply) stay + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + log.info("received query_channel_range_ex2={}", routingMessage) + // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] + val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val blocks = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamps(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + log.info("sending back reply_channel_range_ex2 with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + val replies = blocks.map(block => ReplyChannelRangeEx2(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + replies.foreach(reply => transport ! reply) + stay + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (format, theirShortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) + val (format, theirShortChannelIds, useGzip) = ShortChannelIdsBlock.decode(data) val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) val missing: SortedSet[ShortChannelId] = theirShortChannelIds -- ourShortChannelIds log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={} useGzip={}", missing.size, format, useGzip) @@ -515,7 +548,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case None => // we don't have a pending query with this peer val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip)) + transport ! QueryShortChannelIds(chainHash, router.ShortChannelIdsBlock.encodeSingle(slice, format, useGzip)) d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state @@ -527,7 +560,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (format, theirTimestampMap) = ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(data) + val (format, theirTimestampMap) = ShortChannelIdAndTimestampBlock.decode(data) val theirShortChannelIds = theirTimestampMap.keySet // keep our ids that match [block, block + numberOfBlocks] val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(id => { @@ -551,7 +584,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case None => // we don't have a pending query with this peer val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip = false)) + transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size, outdated, outdated.size))) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state @@ -562,7 +595,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case None => // we don't have a pending query with this peer val (slice, rest) = outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip = false)) + transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet(), 0, outdated, outdated.size))) case Some(sync) => // we already have a pending query with this peer, add outdated ids to our "sync" state @@ -572,9 +605,56 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct context.system.eventStream.publish(syncProgress(d1)) stay using d1 + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + val (format, theirTimestampMap) = ShortChannelIdAndTimestampsBlock.decode(data) + val theirShortChannelIds = theirTimestampMap.keySet + // keep our ids that match [block, block + numberOfBlocks] + val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(id => { + val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) + height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) + }) + + // missing are the ones we don't have + val missing = (theirShortChannelIds -- ourShortChannelIds).map(id => ShortChannelIdAndFlag(id, ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) + + def computeFlag(id: ShortChannelId): Byte = { + val (ourts1, ourts2) = Router.getTimestamps(d.channels, d.updates)(id) + val (theirts1, theirts2) = theirTimestampMap.getOrElse(id, (0L, 0L)) + var flag = 0 + if (ourts1 < theirts1) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 + if (ourts2 < theirts2) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 + flag.toByte + } + + // outdated are the ones for which our update timestamp is older that theirs + val outdated = ourShortChannelIds.filter(id => theirShortChannelIds.contains(id)).map(id => ShortChannelIdAndFlag(id, computeFlag(id))).filterNot(_.flag == 0.toByte) + + // combine missing and outdated sets + val shortChannelIdAndFlags = missing ++ outdated + + log.info("received reply_channel_range_ex2, we have missing {} channel announcements and we have {} outdated ones, format={} ", missing.size, outdated.size, format) + + val d1 = if (shortChannelIdAndFlags.nonEmpty) { + d.sync.get(remoteNodeId) match { + case None => + // we don't have a pending query with this peer + val (slice, rest) = shortChannelIdAndFlags.splitAt(SHORTID_WINDOW) + transport ! QueryShortChannelIdsEx2(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) + d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) + case Some(sync) => + // we already have a pending query with this peer, add missing ids to our "sync" state + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdatedEx = sync.outdatedEx ++ shortChannelIdAndFlags, totalOutdatedExCount = sync.totalOutdatedExCount + shortChannelIdAndFlags.size))) + } + } else d // nothing to do + + context.system.eventStream.publish(syncProgress(d1)) + stay using d1 + + // standard query message: a list of channel ids case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (_, shortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) + val (_, shortChannelIds, useGzip) = ShortChannelIdsBlock.decode(data) log.info("received query_short_channel_ids for {} channel announcements, useGzip={}", shortChannelIds.size, useGzip) shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { @@ -588,9 +668,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct transport ! ReplyShortChannelIdsEnd(chainHash, 1) stay + // extended query message: a flag and a list of channel ids case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsEx(chainHash, flag, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (_, shortChannelIds, _) = ChannelRangeQueries.decodeShortChannelIds(data) + val (_, shortChannelIds, _) = ShortChannelIdsBlock.decode(data) log.info("received query_short_channel_ids_ex for {} channel announcements, flag={}", shortChannelIds.size, flag) shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { @@ -604,6 +685,26 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct transport ! ReplyShortChannelIdsEndEx(chainHash, 1) stay + + // new extended query message: a list of [channel id + flag] + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsEx2(chainHash, data)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + val (_, shortChannelIdAndFlags) = ShortChannelIdAndFlagsBlock.decode(data) + log.info("received query_short_channel_ids_ex2 for {} channel announcements", shortChannelIdAndFlags.size) + shortChannelIdAndFlags.foreach { + case (shortChannelId, flag) => { + d.channels.get(shortChannelId) match { + case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) + case Some(ca) => + if (ChannelRangeQueries.includeAnnoucement(flag)) transport ! ca + if (ChannelRangeQueries.includeAnnoucement(flag) || ChannelRangeQueries.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) + if (ChannelRangeQueries.includeAnnoucement(flag) || ChannelRangeQueries.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) + } + } + } + transport ! ReplyShortChannelIdsEndEx2(chainHash, 1) + stay + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEnd(chainHash, complete)), d) => sender ! TransportHandler.ReadAck(routingMessage) log.info("received reply_short_channel_ids_end={}", routingMessage) @@ -612,7 +713,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case Some(sync) if sync.missing.nonEmpty => log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIds(chainHash, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) case Some(sync) if sync.missing.isEmpty => // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove @@ -632,12 +733,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case Some(sync) if sync.missing.nonEmpty => log.info(s"asking {} for the next slice of missing short_channel_ids", remoteNodeId) val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) case Some(sync) if sync.outdated.nonEmpty => log.info(s"asking {} for the next slice of outdated short_channel_ids", remoteNodeId) val (slice, rest) = sync.outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = rest))) case Some(sync) if sync.missing.isEmpty && sync.outdated.isEmpty => // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove @@ -648,6 +749,27 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct } context.system.eventStream.publish(syncProgress(d1)) stay using d1 + + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndEx2(chainHash, complete)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + log.info("received reply_short_channel_ids_end_ex2={}", routingMessage) + // have we more channels to ask this peer? + val d1 = d.sync.get(remoteNodeId) match { + case Some(sync) if sync.outdatedEx.nonEmpty => + log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) + val (slice, rest) = sync.outdatedEx.splitAt(SHORTID_WINDOW) + transport ! QueryShortChannelIdsEx2(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT)) + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdatedEx = rest))) + case Some(sync) if sync.outdatedEx.isEmpty => + // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove + // the remote peer from our map + d.copy(sync = d.sync - remoteNodeId) + case _ => + d + } + context.system.eventStream.publish(syncProgress(d1)) + stay using d1 + } initialize() @@ -782,7 +904,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = sync.missing + u.shortChannelId, totalMissingCount = sync.totalMissingCount + 1))) case None => // we send the query right away - transport ! QueryShortChannelIds(u.chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(Seq(u.shortChannelId), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIds(u.chainHash, ShortChannelIdsBlock.encodeSingle(Seq(u.shortChannelId), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(missing = SortedSet(u.shortChannelId), totalMissingCount = 1))) } case _ => @@ -876,7 +998,7 @@ object Router { if (d.sync.isEmpty) { SyncProgress(1) } else { - SyncProgress(1 - d.sync.values.map(_.missing.size).sum * 1.0 / d.sync.values.map(_.totalMissingCount).sum) + SyncProgress(d.sync.values.map(_.progress).sum / d.sync.values.size) } /** @@ -907,12 +1029,27 @@ object Router { case (Some(u1), Some(u2)) => Math.max(u1.timestamp, u2.timestamp) case (Some(u1), None) => u1.timestamp case (None, Some(u2)) => u2.timestamp - case (None, None) => - 0L + case (None, None) => 0L } timestamp } + def getTimestamps(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(id: ShortChannelId): (Long, Long) = { + val ca = channels(id) + val opt1 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)) + val opt2 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)) + val timestamps = (opt1, opt2) match { + case (Some(u1), Some(u2)) if Announcements.isNode1(u1.channelFlags) && !Announcements.isNode1(u2.channelFlags) => (u1.timestamp, u2.timestamp) + case (Some(u1), Some(u2)) if !Announcements.isNode1(u1.channelFlags) && Announcements.isNode1(u2.channelFlags) => (u2.timestamp, u1.timestamp) + case (Some(u1), None) if Announcements.isNode1(u1.channelFlags) => (u1.timestamp, 0L) + case (Some(u1), None) => (0L, u1.timestamp) + case (None, Some(u2)) if Announcements.isNode1(u2.channelFlags) => (u2.timestamp, 0L) + case (None, Some(u2)) => (0L, u2.timestamp) + case (None, None) => (0L, 0L) + } + timestamps + } + /** * Routing fee have a variable part, as a simplification we compute fees using a default constant value for the amount */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala similarity index 64% rename from eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala index 9243afdd7b..8d2ed21016 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala @@ -2,33 +2,31 @@ package fr.acinq.eclair.router import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} import java.nio.ByteOrder -import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream} +import java.util.zip.{DeflaterOutputStream, InflaterInputStream} import fr.acinq.bitcoin.{BinaryData, Protocol} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.router.ChannelRangeQueries.{UNCOMPRESSED_FORMAT, ZLIB_FORMAT, readBytes} import scala.annotation.tailrec import scala.collection.SortedSet import scala.collection.immutable.SortedMap -object ChannelRangeQueriesEx { - val UNCOMPRESSED_FORMAT = 0.toByte - val ZLIB_FORMAT = 1.toByte - - case class ShortChannelIdAndTimestampsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: BinaryData) +case class ShortChannelIdAndTimestampBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: BinaryData) +object ShortChannelIdAndTimestampBlock { /** * Compressed a sequence of *sorted* short channel id. * * @param shortChannelIds must be sorted beforehand * @return a sequence of encoded short channel ids */ - def encodeShortChannelIdAndTimestamps(firstBlockIn: Long, numBlocksIn: Long, - shortChannelIds: SortedSet[ShortChannelId], timestamp: ShortChannelId => Long, - format: Byte): List[ShortChannelIdAndTimestampsBlock] = { + def encode(firstBlockIn: Long, numBlocksIn: Long, + shortChannelIds: SortedSet[ShortChannelId], timestamp: ShortChannelId => Long, + format: Byte): List[ShortChannelIdAndTimestampBlock] = { if (shortChannelIds.isEmpty) { // special case: reply with an "empty" block - List(ShortChannelIdAndTimestampsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) + List(ShortChannelIdAndTimestampBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) } else { // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid val count = format match { @@ -41,13 +39,13 @@ object ChannelRangeQueriesEx { val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 (firstBlock, numBlocks) } - val encoded = encodeShortChannelIdAndTimestampsSingle(ids, timestamp, format) - ShortChannelIdAndTimestampsBlock(firstBlock, numBlocks, encoded) + val encoded = encodeSingle(ids, timestamp, format) + ShortChannelIdAndTimestampBlock(firstBlock, numBlocks, encoded) }).toList } } - def encodeShortChannelIdAndTimestampsSingle(shortChannelIds: Iterable[ShortChannelId], timestamp: ShortChannelId => Long, format: Byte): BinaryData = { + def encodeSingle(shortChannelIds: Iterable[ShortChannelId], timestamp: ShortChannelId => Long, format: Byte): BinaryData = { val bos = new ByteArrayOutputStream() bos.write(format) val out = format match { @@ -61,33 +59,22 @@ object ChannelRangeQueriesEx { out.close() bos.toByteArray } - + /** * Decompress a zipped sequence of sorted [short channel id | timestamp] values. * * @param data * @return a sorted map of short channel id -> timestamp */ - def decodeShortChannelIdAndTimestamps(data: BinaryData): (Byte, SortedMap[ShortChannelId, Long]) = { + def decode(data: BinaryData): (Byte, SortedMap[ShortChannelId, Long]) = { val format = data.head if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, Long]) else { val buffer = new Array[Byte](12) - // read 12 bytes from input - // zipped input stream often returns less bytes than what you want to read - @tailrec - def read12(input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, 12 - offset) match { - case len if len <= 0 => len - case 12 => 12 - case len if offset + len == 12 => 12 - case len => read12(input, offset + len) - } - - // read until there's nothing left @tailrec def loop(input: InputStream, acc: SortedMap[ShortChannelId, Long]): SortedMap[ShortChannelId, Long] = { - val check = read12(input) + val check = readBytes(buffer, input) if (check <= 0) acc else loop(input, acc + (ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) -> Protocol.uint32(buffer.drop(8), ByteOrder.BIG_ENDIAN))) } @@ -109,3 +96,4 @@ object ChannelRangeQueriesEx { } } } + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala new file mode 100644 index 0000000000..35f2c0975e --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala @@ -0,0 +1,226 @@ +package fr.acinq.eclair.router + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, OutputStream} +import java.nio.ByteOrder +import java.util.zip.{DeflaterOutputStream, InflaterInputStream} + +import fr.acinq.bitcoin.{BinaryData, Protocol} +import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.router.ChannelRangeQueries._ + +import scala.annotation.tailrec +import scala.collection.SortedSet +import scala.collection.immutable.SortedMap + +/** + * + * @param firstBlock fist block covered by this message + * @param numBlocks number of blocks covered by this message + * @param shortChannelIdAndTimestamps packed list of short channel id + timestamp #1 + timestamp #2 + */ +case class ShortChannelIdAndTimestampsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: BinaryData) + +object ShortChannelIdAndTimestampsBlock { + /** + * Compressed a sequence of *sorted* short channel id and timestamps + * + * @param shortChannelIds must be sorted beforehand + * @return a sequence of encoded short channel ids + */ + def encode(firstBlockIn: Long, numBlocksIn: Long, + shortChannelIds: SortedSet[ShortChannelId], timestamp: ShortChannelId => (Long, Long), + format: Byte): List[ShortChannelIdAndTimestampsBlock] = { + if (shortChannelIds.isEmpty) { + // special case: reply with an "empty" block + List(ShortChannelIdAndTimestampsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) + } else { + // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + val count = format match { + case UNCOMPRESSED_FORMAT => 2000 + case ZLIB_FORMAT => 5000 // TODO: do something less simplistic... + } + shortChannelIds.grouped(count).map(ids => { + val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { + val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight + val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 + (firstBlock, numBlocks) + } + val encoded = encodeSingle(ids, timestamp, format) + ShortChannelIdAndTimestampsBlock(firstBlock, numBlocks, encoded) + }).toList + } + } + + def writeChannelIdAndTimestamps(out: OutputStream, id: ShortChannelId, timestamp1: Long, timestamp2: Long): Unit = { + Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN) + Protocol.writeUInt32(timestamp1, out, ByteOrder.BIG_ENDIAN) + Protocol.writeUInt32(timestamp2, out, ByteOrder.BIG_ENDIAN) + } + + def writeChannelIdAndTimestamps(out: OutputStream, id: ShortChannelId, timestamp: ShortChannelId => (Long, Long)): Unit = { + val (ts1, ts2) = timestamp(id) + writeChannelIdAndTimestamps(out, id, ts1, ts2) + } + + def encodeSingle(shortChannelIds: Iterable[ShortChannelId], timestamp: ShortChannelId => (Long, Long), format: Byte): BinaryData = { + val bos = new ByteArrayOutputStream() + bos.write(format) + val out = format match { + case UNCOMPRESSED_FORMAT => bos + case ZLIB_FORMAT => new DeflaterOutputStream(bos) + } + shortChannelIds.foreach(id => writeChannelIdAndTimestamps(out, id, timestamp)) + out.close() + bos.toByteArray + } + + /** + * Decompress a zipped sequence of sorted [short channel id | timestamp1 | timestamp2 ] values. + * + * @param data + * @return a sorted map of short channel id -> timestamps + */ + def decode(data: BinaryData): (Byte, SortedMap[ShortChannelId, (Long, Long)]) = { + val format = data.head + if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, (Long, Long)]) else { + val buffer = new Array[Byte](16) + + // read until there's nothing left + @tailrec + def loop(input: InputStream, acc: SortedMap[ShortChannelId, (Long, Long)]): SortedMap[ShortChannelId, (Long, Long)] = { + val check = readBytes(buffer, input) + if (check <= 0) acc else { + val id = ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) + val ts1 = Protocol.uint32(buffer.drop(8), ByteOrder.BIG_ENDIAN) + val ts2 = Protocol.uint32(buffer.drop(12), ByteOrder.BIG_ENDIAN) + loop(input, acc + (id -> (ts1, ts2))) + } + } + + def readAll() = { + val bis = new ByteArrayInputStream(data.tail.toArray) + val input = format match { + case UNCOMPRESSED_FORMAT => bis + case ZLIB_FORMAT => new InflaterInputStream(bis) + } + try { + (format, loop(input, SortedMap.empty[ShortChannelId, (Long, Long)])) + } + finally { + input.close() + } + } + + readAll() + } + } +} + +case class ShortChannelIdAndFlag(id: ShortChannelId, flag: Byte) extends Ordered[ShortChannelIdAndFlag] { + override def compare(that: ShortChannelIdAndFlag): Int = this.id.compare(that.id) +} + +case class ShortChannelIdAndFlagsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndFlags: BinaryData) + +object ShortChannelIdAndFlagsBlock { + /** + * Compressed a sequence of *sorted* short channel id. + * + * @param shortChannelIds must be sorted beforehand + * @return a sequence of encoded short channel ids + */ + def encode(firstBlockIn: Long, numBlocksIn: Long, + shortChannelIds: SortedSet[ShortChannelId], flag: ShortChannelId => Byte, + format: Byte): List[ShortChannelIdAndFlagsBlock] = { + if (shortChannelIds.isEmpty) { + // special case: reply with an "empty" block + List(ShortChannelIdAndFlagsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) + } else { + // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + val count = format match { + case UNCOMPRESSED_FORMAT => 2000 + case ZLIB_FORMAT => 5000 // TODO: do something less simplistic... + } + shortChannelIds.grouped(count).map(ids => { + val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { + val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight + val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 + (firstBlock, numBlocks) + } + val encoded = encodeSingle(ids, flag, format) + ShortChannelIdAndFlagsBlock(firstBlock, numBlocks, encoded) + }).toList + } + } + + def encodeSingle(shortChannelIds: Iterable[ShortChannelId], flag: ShortChannelId => Byte, format: Byte): BinaryData = { + val bos = new ByteArrayOutputStream() + bos.write(format) + val out = format match { + case UNCOMPRESSED_FORMAT => bos + case ZLIB_FORMAT => new DeflaterOutputStream(bos) + } + shortChannelIds.foreach(id => { + Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN) + Protocol.writeUInt8(flag(id), out) + }) + out.close() + bos.toByteArray + } + + def encodeSingle(shortChannelIdAndFlags: SortedSet[ShortChannelIdAndFlag], format: Byte): BinaryData = { + val bos = new ByteArrayOutputStream() + bos.write(format) + val out = format match { + case UNCOMPRESSED_FORMAT => bos + case ZLIB_FORMAT => new DeflaterOutputStream(bos) + } + shortChannelIdAndFlags.foreach(value => { + Protocol.writeUInt64(value.id.toLong, out, ByteOrder.BIG_ENDIAN) + Protocol.writeUInt8(value.flag, out) + }) + out.close() + bos.toByteArray + } + + /** + * Decompress a zipped sequence of sorted [short channel id | timestamp1 | timestamp2 ] values. + * + * @param data + * @return a sorted map of short channel id -> timestamps + */ + def decode(data: BinaryData): (Byte, SortedMap[ShortChannelId, Byte]) = { + val format = data.head + if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, Byte]) else { + val buffer = new Array[Byte](9) + + // read until there's nothing left + @tailrec + def loop(input: InputStream, acc: SortedMap[ShortChannelId, Byte]): SortedMap[ShortChannelId, Byte] = { + val check = readBytes(buffer, input) + if (check <= 0) acc else { + val id = ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) + val flag = buffer(8) + loop(input, acc + (id -> flag)) + } + } + + def readAll() = { + val bis = new ByteArrayInputStream(data.tail.toArray) + val input = format match { + case UNCOMPRESSED_FORMAT => bis + case ZLIB_FORMAT => new InflaterInputStream(bis) + } + try { + (format, loop(input, SortedMap.empty[ShortChannelId, Byte])) + } + finally { + input.close() + } + } + + readAll() + } + } +} + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala new file mode 100644 index 0000000000..452981d7c1 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala @@ -0,0 +1,99 @@ +package fr.acinq.eclair.router + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} +import java.nio.ByteOrder +import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream} + +import fr.acinq.bitcoin.{BinaryData, Protocol} +import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.router.ChannelRangeQueries.{UNCOMPRESSED_FORMAT, ZLIB_FORMAT, readBytes} + +import scala.annotation.tailrec +import scala.collection.SortedSet + +case class ShortChannelIdsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIds: BinaryData) + +object ShortChannelIdsBlock { + /** + * Compressed a sequence of *sorted* short channel id. + * + * @param shortChannelIds must be sorted beforehand + * @return a sequence of short channel id blocks + */ + def encode(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false): List[ShortChannelIdsBlock] = { + if (shortChannelIds.isEmpty) { + // special case: reply with an "empty" block + List(ShortChannelIdsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) + } else { + // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + val count = format match { + case UNCOMPRESSED_FORMAT => 7000 + case ZLIB_FORMAT => 12000 // TODO: do something less simplistic... + } + shortChannelIds.grouped(count).map(ids => { + val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { + val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight + val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 + (firstBlock, numBlocks) + } + val encoded = encodeSingle(ids, format, useGzip) + ShortChannelIdsBlock(firstBlock, numBlocks, encoded) + }).toList + } + } + + def encodeSingle(shortChannelIds: Iterable[ShortChannelId], format: Byte, useGzip: Boolean): BinaryData = { + val bos = new ByteArrayOutputStream() + bos.write(format) + val out = format match { + case UNCOMPRESSED_FORMAT => bos + case ZLIB_FORMAT => if (useGzip) new GZIPOutputStream(bos) else new DeflaterOutputStream(bos) + } + shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN)) + out.close() + bos.toByteArray + } + + /** + * Decompress a zipped sequence of sorted short channel ids. + * + * @param data + * @return a sorted set of short channel ids + */ + def decode(data: BinaryData): (Byte, SortedSet[ShortChannelId], Boolean) = { + val format = data.head + if (data.tail.isEmpty) (format, SortedSet.empty[ShortChannelId], false) else { + val buffer = new Array[Byte](8) + + // read until there's nothing left + @tailrec + def loop(input: InputStream, acc: SortedSet[ShortChannelId]): SortedSet[ShortChannelId] = { + val check = readBytes(buffer, input) + if (check <= 0) acc else loop(input, acc + ShortChannelId(Protocol.uint64(buffer, ByteOrder.BIG_ENDIAN))) + } + + def readAll(useGzip: Boolean) = { + val bis = new ByteArrayInputStream(data.tail.toArray) + val input = format match { + case UNCOMPRESSED_FORMAT => bis + case ZLIB_FORMAT if useGzip => new GZIPInputStream(bis) + case ZLIB_FORMAT => new InflaterInputStream(bis) + } + try { + (format, loop(input, SortedSet.empty[ShortChannelId]), useGzip) + } + finally { + input.close() + } + } + + try { + readAll(useGzip = false) + } + catch { + case _: Throwable if format == ZLIB_FORMAT => readAll(useGzip = true) + } + } + } +} + 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 da41eed0fa..8cdb387df2 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 @@ -328,23 +328,47 @@ object LightningMessageCodecs { ).as[QueryShortChannelIdsEx] val replyShortChanelIdsEndExCodec: Codec[ReplyShortChannelIdsEndEx] = ( + ("chainHash" | binarydata(32)) :: + ("complete" | byte) + ).as[ReplyShortChannelIdsEndEx] + + val queryChannelRangeExCodec: Codec[QueryChannelRangeEx] = ( + ("chainHash" | binarydata(32)) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) + ).as[QueryChannelRangeEx] + + val replyChannelRangeExCodec: Codec[ReplyChannelRangeEx] = ( + ("chainHash" | binarydata(32)) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("complete" | byte) :: + ("data" | varsizebinarydata) + ).as[ReplyChannelRangeEx] + + val queryShortChannelIdsEx2Codec: Codec[QueryShortChannelIdsEx2] = ( + ("chainHash" | binarydata(32)) :: + ("data" | varsizebinarydata) + ).as[QueryShortChannelIdsEx2] + + val replyShortChanelIdsEndEx2Codec: Codec[ReplyShortChannelIdsEndEx2] = ( ("chainHash" | binarydata(32)) :: ("complete" | byte) - ).as[ReplyShortChannelIdsEndEx] + ).as[ReplyShortChannelIdsEndEx2] - val queryChannelRangeExCodec: Codec[QueryChannelRangeEx] = ( + val queryChannelRangeEx2Codec: Codec[QueryChannelRangeEx2] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) - ).as[QueryChannelRangeEx] + ).as[QueryChannelRangeEx2] - val replyChannelRangeExCodec: Codec[ReplyChannelRangeEx] = ( + val replyChannelRangeEx2Codec: Codec[ReplyChannelRangeEx2] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: ("complete" | byte) :: ("data" | varsizebinarydata) - ).as[ReplyChannelRangeEx] + ).as[ReplyChannelRangeEx2] val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( ("chainHash" | binarydata(32)) :: @@ -385,6 +409,10 @@ object LightningMessageCodecs { .typecase(1002, replyShortChanelIdsEndExCodec) .typecase(1003, queryChannelRangeExCodec) .typecase(1004, replyChannelRangeExCodec) + .typecase(1011, queryShortChannelIdsEx2Codec) + .typecase(1012, replyShortChanelIdsEndEx2Codec) + .typecase(1013, queryChannelRangeEx2Codec) + .typecase(1014, replyChannelRangeEx2Codec) /** 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 469460eb74..707491a290 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 @@ -225,6 +225,8 @@ case class QueryShortChannelIdsEx(chainHash: BinaryData, flag: Byte, data: BinaryData) extends RoutingMessage with HasChainHash +case class QueryShortChannelIdsEx2(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash + case class QueryChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash @@ -233,6 +235,10 @@ case class QueryChannelRangeEx(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash +case class QueryChannelRangeEx2(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long) extends RoutingMessage with HasChainHash + /** * * @param chainHash chain hash @@ -261,12 +267,21 @@ case class ReplyChannelRangeEx(chainHash: BinaryData, complete: Byte, data: BinaryData) extends RoutingMessage with HasChainHash +case class ReplyChannelRangeEx2(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash + case class ReplyShortChannelIdsEnd(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash case class ReplyShortChannelIdsEndEx(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash +case class ReplyShortChannelIdsEndEx2(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash + case class GossipTimestampFilter(chainHash: BinaryData, firstTimestamp: Long, timestampRange: Long) extends RoutingMessage with HasChainHash \ No newline at end of file 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 347d86a4bb..e6e2550a1c 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 @@ -10,7 +10,7 @@ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} +import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast, ShortChannelIdsBlock} import fr.acinq.eclair.wire.Error import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, wire} import org.scalatest.Outcome @@ -101,7 +101,7 @@ class PeerSpec extends TestkitBaseClass { val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) - val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(Seq(ShortChannelId(42000)), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, ShortChannelIdsBlock.encodeSingle(Seq(ShortChannelId(42000)), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) // make sure that routing messages go through for (ann <- channels ++ updates) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala index f300417c9c..4f7f7f27fc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala @@ -14,19 +14,19 @@ class ChannelRangeQueriesExSpec extends FunSuite { val timestamps = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap test("create `reply_channel_range_ex` messages (uncompressed format)") { - val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(reply.data)._2) + replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) shortChannelIds.foreach(id => timestamps(id) == decoded(id)) } test("create `reply_channel_range_ex` messages (zlib format)") { - val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) + val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(reply.data)._2) + replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) shortChannelIds.foreach(id => timestamps(id) == decoded(id)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 4e88450720..aca9bf7b9c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -2,55 +2,98 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Block import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.ReplyChannelRange +import fr.acinq.eclair.wire.{ReplyChannelRange, ReplyChannelRangeEx} import org.scalatest.FunSuite +import scala.collection.immutable.SortedMap import scala.collection.{SortedSet, immutable} +import scala.util.Random class ChannelRangeQueriesSpec extends FunSuite { import ChannelRangeQueriesSpec._ + val random = new Random() + val timestamp = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap + val timestamps = shortChannelIds.map(id => id -> (random.nextInt(400000).toLong, random.nextInt(400000).toLong)).toMap + test("create `reply_channel_range` messages (uncompressed format)") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ ChannelRangeQueries.decodeShortChannelIds(reply.data)._2) + replies.foreach(reply => decoded = decoded ++ ShortChannelIdsBlock.decode(reply.data)._2) assert(decoded == shortChannelIds) } test("create `reply_channel_range` messages (ZLIB format)") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) + val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) var decoded = Set.empty[ShortChannelId] replies.foreach(reply => decoded = decoded ++ { - val (ChannelRangeQueries.ZLIB_FORMAT, ids, false) = ChannelRangeQueries.decodeShortChannelIds(reply.data) + val (ChannelRangeQueries.ZLIB_FORMAT, ids, false) = ShortChannelIdsBlock.decode(reply.data) ids }) assert(decoded == shortChannelIds) } test("create `reply_channel_range` messages (GZIP format)") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = true) + val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = true) val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) var decoded = Set.empty[ShortChannelId] replies.foreach(reply => decoded = decoded ++ { - val (ChannelRangeQueries.ZLIB_FORMAT, ids, true) = ChannelRangeQueries.decodeShortChannelIds(reply.data) + val (ChannelRangeQueries.ZLIB_FORMAT, ids, true) = ShortChannelIdsBlock.decode(reply.data) ids }) assert(decoded == shortChannelIds) } test("create empty `reply_channel_range` message") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, SortedSet.empty[ShortChannelId], ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) + val blocks = ShortChannelIdsBlock.encode(400000, 20000, SortedSet.empty[ShortChannelId], ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) var decoded = Set.empty[ShortChannelId] replies.foreach(reply => decoded = decoded ++ { - val (format, ids, false) = ChannelRangeQueries.decodeShortChannelIds(reply.data) + val (format, ids, false) = ShortChannelIdsBlock.decode(reply.data) ids }) assert(decoded.isEmpty) } + + test("create `reply_channel_range_ex` messages (uncompressed format)") { + val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + var decoded = SortedMap.empty[ShortChannelId, Long] + replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) + assert(decoded.keySet == shortChannelIds) + shortChannelIds.foreach(id => timestamp(id) == decoded(id)) + } + + test("create `reply_channel_range_ex` messages (zlib format)") { + val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.ZLIB_FORMAT) + val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + var decoded = SortedMap.empty[ShortChannelId, Long] + replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) + assert(decoded.keySet == shortChannelIds) + shortChannelIds.foreach(id => timestamp(id) == decoded(id)) + } + + test("create `reply_channel_range_ex2` messages (uncompressed format)") { + val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] + replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) + assert(decoded.keySet == shortChannelIds) + shortChannelIds.foreach(id => timestamp(id) == decoded(id)) + } + + test("create `reply_channel_range_ex2` messages (zlib format)") { + val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) + val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] + replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) + assert(decoded.keySet == shortChannelIds) + shortChannelIds.foreach(id => timestamp(id) == decoded(id)) + } + } object ChannelRangeQueriesSpec { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index d18c69ce34..f89991d1f2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -273,6 +273,6 @@ class RouterSpec extends BaseRouterSpec { val transport = TestProbe() probe.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, update1)) val query = transport.expectMsgType[QueryShortChannelIds] - assert(ChannelRangeQueries.decodeShortChannelIds(query.data)._2 == SortedSet(channelId)) + assert(ShortChannelIdsBlock.decode(query.data)._2 == SortedSet(channelId)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala new file mode 100644 index 0000000000..6f595eb589 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala @@ -0,0 +1,84 @@ +package fr.acinq.eclair.router + +import akka.actor.ActorSystem +import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import fr.acinq.eclair._ +import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.wire._ +import org.scalatest.FunSuiteLike + +import scala.collection.immutable.TreeMap +import scala.concurrent.duration._ + + +class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike { + import RoutingSyncSpec.makeFakeRoutingInfo + + test("handle chanel range extended queries") { + val params = TestConstants.Alice.nodeParams + val router = TestFSMRef(new Router(params, TestProbe().ref)) + val sender = TestProbe() + sender.ignoreMsg { case _: TransportHandler.ReadAck => true } + val remoteNodeId = TestConstants.Bob.nodeParams.nodeId + + // ask router to send a channel range query + sender.send(router, SendChannelQueryEx2(remoteNodeId, sender.ref)) + val QueryChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeEx2] + sender.expectMsgType[GossipTimestampFilter] + + + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) + val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap + val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } + val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => + val desc = Router.getDesc(u, initChannels(u.shortChannelId)) + (desc) -> u + }.toMap + + // split our anwser in 3 blocks + val List(block1) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block2) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block3) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + + // send first block + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) + // router should ask for our first block of ids + val QueryShortChannelIdsEx2(_, data1) = sender.expectMsgType[QueryShortChannelIdsEx2] + val (_, shortChannelIdAndFlags1) = ShortChannelIdAndFlagsBlock.decode(data1) + assert(shortChannelIdAndFlags1.keySet == shortChannelIds.take(100)) + assert(shortChannelIdAndFlags1.values.toSet == Set(ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) + + // send second block + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) + + // router should not ask for more ids, it already has a pending query ! + sender.expectNoMsg(1 second) + + // send the first 50 items + shortChannelIdAndFlags1.take(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) + }) + sender.expectNoMsg(1 second) + + // send the last 50 items + shortChannelIdAndFlags1.drop(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) + }) + sender.expectNoMsg(1 second) + + // now send our ReplyShortChannelIdsEnd message + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + + // router should ask for our second block of ids + val QueryShortChannelIdsEx2(_, data2) = sender.expectMsgType[QueryShortChannelIdsEx2] + val (_, shortChannelIds2) = ShortChannelIdAndFlagsBlock.decode(data2) + assert(shortChannelIds2.keySet == shortChannelIds.drop(100).take(100)) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala index a017241d77..e43277eab8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala @@ -37,15 +37,15 @@ class RoutingSyncExSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { }.toMap // split our anwser in 3 blocks - val List(block1) = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueriesEx.UNCOMPRESSED_FORMAT) - val List(block2) = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueriesEx.UNCOMPRESSED_FORMAT) - val List(block3) = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueriesEx.UNCOMPRESSED_FORMAT) + val List(block1) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block2) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block3) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) // send first block sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) // router should ask for our first block of ids val QueryShortChannelIdsEx(_, _, data1) = sender.expectMsgType[QueryShortChannelIdsEx] - val (_, shortChannelIds1, false) = ChannelRangeQueries.decodeShortChannelIds(data1) + val (_, shortChannelIds1, false) = ShortChannelIdsBlock.decode(data1) assert(shortChannelIds1 == shortChannelIds.take(100)) // send second block @@ -77,7 +77,7 @@ class RoutingSyncExSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { // router should ask for our second block of ids val QueryShortChannelIdsEx(_, _, data2) = sender.expectMsgType[QueryShortChannelIdsEx] - val (_, shortChannelIds2, false) = ChannelRangeQueries.decodeShortChannelIds(data2) + val (_, shortChannelIds2, false) = ShortChannelIdsBlock.decode(data2) assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index ff5c861dca..78952a2c38 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -36,15 +36,15 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.expectMsgType[GossipTimestampFilter] // split our answer in 3 blocks - val List(block1) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block2) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block3) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block1) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block2) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block3) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), ChannelRangeQueries.UNCOMPRESSED_FORMAT) // send first block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) // router should ask for our first block of ids val QueryShortChannelIds(_, data1) = transport.expectMsgType[QueryShortChannelIds] - val (_, shortChannelIds1, false) = ChannelRangeQueries.decodeShortChannelIds(data1) + val (_, shortChannelIds1, false) = ShortChannelIdsBlock.decode(data1) assert(shortChannelIds1 == shortChannelIds.take(100)) // send second block @@ -74,7 +74,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { // router should ask for our second block of ids val QueryShortChannelIds(_, data2) = transport.expectMsgType[QueryShortChannelIds] - val (_, shortChannelIds2, false) = ChannelRangeQueries.decodeShortChannelIds(data2) + val (_, shortChannelIds2, false) = ShortChannelIdsBlock.decode(data2) assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) } @@ -91,7 +91,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - val List(block1) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val List(block1) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) // send first block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) From 4f513c4bae2d3a74bbaa52ddadebd286fb90ad11 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 29 Oct 2018 17:30:29 +0100 Subject: [PATCH 16/86] Extended channel queries: refactor handlers, add more tests --- .../scala/fr/acinq/eclair/router/Router.scala | 107 ++++++++------ .../eclair/router/RouterSyncEx2Spec.scala | 131 ++++++++++++++---- .../acinq/eclair/router/RoutingSyncSpec.scala | 21 ++- 3 files changed, 188 insertions(+), 71 deletions(-) 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 b740a5b442..b9b86431d0 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 @@ -90,6 +90,66 @@ case class Data(nodes: Map[PublicKey, NodeAnnouncement], // for which we have not yet received an 'end' message ) +object Data { + /** + * Process a reply_channel_range message + * @param state current state + * @param remoteNodeId id of the node that sent the reply + * @param replyChannelRangeEx2 reply_channel_range message + * @return a (new state, query) pair, where query is an optional query_short_channel_ids message to be sent to be + * sent back to the remote node + */ + def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx2: ReplyChannelRangeEx2): (Data, Option[QueryShortChannelIdsEx2]) = { + val ReplyChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks, _, data) = replyChannelRangeEx2 + val (format, theirTimestampMap) = ShortChannelIdAndTimestampsBlock.decode(data) + val theirShortChannelIds = theirTimestampMap.keySet + + // keep our ids that match [block, block + numberOfBlocks] + val ourShortChannelIds: SortedSet[ShortChannelId] = state.channels.keySet.filter(id => { + val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) + height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) + }) + + // missing are the ones we don't have + val missing = (theirShortChannelIds -- ourShortChannelIds).map(id => ShortChannelIdAndFlag(id, ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) + + def computeFlag(id: ShortChannelId): Byte = { + val (ourts1, ourts2) = Router.getTimestamps(state.channels, state.updates)(id) + val (theirts1, theirts2) = theirTimestampMap.getOrElse(id, (0L, 0L)) + var flag = 0 + if (ourts1 < theirts1) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 + if (ourts2 < theirts2) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 + flag.toByte + } + + // outdated are the ones for which our update timestamp is older that theirs + val outdated = ourShortChannelIds + .filter(id => theirShortChannelIds.contains(id)) + .map(id => ShortChannelIdAndFlag(id, computeFlag(id))) + .filterNot(_.flag == 0.toByte) + + // combine missing and outdated sets + val shortChannelIdAndFlags = missing ++ outdated + + val (newState, query) = if (shortChannelIdAndFlags.nonEmpty) { + state.sync.get(remoteNodeId) match { + case None => + // we don't have a pending query with this peer + val (slice, rest) = shortChannelIdAndFlags.splitAt(Router.SHORTID_WINDOW) + val query = QueryShortChannelIdsEx2(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) + val state1 = state.copy(sync = state.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) + (state1, Some(query)) + case Some(sync) => + // we already have a pending query with this peer, add missing ids to our "sync" state + val state1 = state.copy(sync = state.sync + (remoteNodeId -> sync.copy(outdatedEx = sync.outdatedEx ++ shortChannelIdAndFlags, totalOutdatedExCount = sync.totalOutdatedExCount + shortChannelIdAndFlags.size))) + (state1, None) + } + } else (state, None) // nothing to do + + (newState, query) + } +} + sealed trait State case object NORMAL extends State @@ -560,6 +620,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) + val (format, theirTimestampMap) = ShortChannelIdAndTimestampBlock.decode(data) val theirShortChannelIds = theirTimestampMap.keySet // keep our ids that match [block, block + numberOfBlocks] @@ -607,47 +668,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (format, theirTimestampMap) = ShortChannelIdAndTimestampsBlock.decode(data) - val theirShortChannelIds = theirTimestampMap.keySet - // keep our ids that match [block, block + numberOfBlocks] - val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(id => { - val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) - height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) - }) - - // missing are the ones we don't have - val missing = (theirShortChannelIds -- ourShortChannelIds).map(id => ShortChannelIdAndFlag(id, ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) - - def computeFlag(id: ShortChannelId): Byte = { - val (ourts1, ourts2) = Router.getTimestamps(d.channels, d.updates)(id) - val (theirts1, theirts2) = theirTimestampMap.getOrElse(id, (0L, 0L)) - var flag = 0 - if (ourts1 < theirts1) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 - if (ourts2 < theirts2) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 - flag.toByte - } - - // outdated are the ones for which our update timestamp is older that theirs - val outdated = ourShortChannelIds.filter(id => theirShortChannelIds.contains(id)).map(id => ShortChannelIdAndFlag(id, computeFlag(id))).filterNot(_.flag == 0.toByte) - - // combine missing and outdated sets - val shortChannelIdAndFlags = missing ++ outdated - - log.info("received reply_channel_range_ex2, we have missing {} channel announcements and we have {} outdated ones, format={} ", missing.size, outdated.size, format) - - val d1 = if (shortChannelIdAndFlags.nonEmpty) { - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = shortChannelIdAndFlags.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx2(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdatedEx = sync.outdatedEx ++ shortChannelIdAndFlags, totalOutdatedExCount = sync.totalOutdatedExCount + shortChannelIdAndFlags.size))) - } - } else d // nothing to do - + val (d1, query) = Data.handleReplyChannelRange(d, remoteNodeId, routingMessage) + query.map(q => transport ! q) context.system.eventStream.publish(syncProgress(d1)) stay using d1 @@ -697,6 +719,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => if (ChannelRangeQueries.includeAnnoucement(flag)) transport ! ca + // specs says that in channel announcements we have node_id_1 transport ! u) if (ChannelRangeQueries.includeAnnoucement(flag) || ChannelRangeQueries.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } @@ -926,6 +950,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct } object Router { + val SHORTID_WINDOW = 100 def props(nodeParams: NodeParams, watcher: ActorRef) = Props(new Router(nodeParams, watcher)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala index 6f595eb589..a700e3bd9d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala @@ -1,10 +1,14 @@ package fr.acinq.eclair.router -import akka.actor.ActorSystem +import akka.actor.{Actor, ActorSystem, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ import org.scalatest.FunSuiteLike @@ -13,11 +17,35 @@ import scala.concurrent.duration._ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike { + import RoutingSyncSpec.makeFakeRoutingInfo - test("handle chanel range extended queries") { + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) + val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap + val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } + val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => + val desc = Router.getDesc(u, initChannels(u.shortChannelId)) + (desc) -> u + }.toMap + + class FakeWatcher extends Actor { + override def receive: Receive = { + case ValidateRequest(c) => + val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) + val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) + val fakeFundingTx = Transaction( + version = 2, + txIn = Seq.empty[TxIn], + txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format + lockTime = 0) + sender ! ValidateResult(c, Some(fakeFundingTx), true, None) + } + } + + test("handle channel range extended (full sync)") { val params = TestConstants.Alice.nodeParams - val router = TestFSMRef(new Router(params, TestProbe().ref)) + val watcher = system.actorOf(Props(new FakeWatcher())) + val router = TestFSMRef(new Router(params, watcher)) val sender = TestProbe() sender.ignoreMsg { case _: TransportHandler.ReadAck => true } val remoteNodeId = TestConstants.Bob.nodeParams.nodeId @@ -27,34 +55,16 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike val QueryChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeEx2] sender.expectMsgType[GossipTimestampFilter] + // send back all our ids and timestamps + val List(block) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps))) - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => - val desc = Router.getDesc(u, initChannels(u.shortChannelId)) - (desc) -> u - }.toMap - - // split our anwser in 3 blocks - val List(block1) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block2) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block3) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - - // send first block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) // router should ask for our first block of ids val QueryShortChannelIdsEx2(_, data1) = sender.expectMsgType[QueryShortChannelIdsEx2] val (_, shortChannelIdAndFlags1) = ShortChannelIdAndFlagsBlock.decode(data1) - assert(shortChannelIdAndFlags1.keySet == shortChannelIds.take(100)) + assert(shortChannelIdAndFlags1.keySet == shortChannelIds.take(Router.SHORTID_WINDOW)) assert(shortChannelIdAndFlags1.values.toSet == Set(ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) - // send second block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) - - // router should not ask for more ids, it already has a pending query ! - sender.expectNoMsg(1 second) - // send the first 50 items shortChannelIdAndFlags1.take(50).foreach(id => { val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) @@ -78,7 +88,74 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike // router should ask for our second block of ids val QueryShortChannelIdsEx2(_, data2) = sender.expectMsgType[QueryShortChannelIdsEx2] - val (_, shortChannelIds2) = ShortChannelIdAndFlagsBlock.decode(data2) - assert(shortChannelIds2.keySet == shortChannelIds.drop(100).take(100)) + val (_, shortChannelIdAndFlags2) = ShortChannelIdAndFlagsBlock.decode(data2) + assert(shortChannelIdAndFlags2.keySet == shortChannelIds.drop(Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) + + // send block #2 + shortChannelIdAndFlags2.foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) + }) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + + // router should ask for our third block of ids + val QueryShortChannelIdsEx2(_, data3) = sender.expectMsgType[QueryShortChannelIdsEx2] + val (_, shortChannelIdAndFlags3) = ShortChannelIdAndFlagsBlock.decode(data3) + assert(shortChannelIdAndFlags3.keySet == shortChannelIds.drop(2 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) + + // send block #3 + shortChannelIdAndFlags3.foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) + }) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + + // router should ask for our third block of ids + val QueryShortChannelIdsEx2(_, data4) = sender.expectMsgType[QueryShortChannelIdsEx2] + val (_, shortChannelIdAndFlags4) = ShortChannelIdAndFlagsBlock.decode(data4) + assert(shortChannelIdAndFlags4.keySet == shortChannelIds.drop(3 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) + // send block #4 + shortChannelIdAndFlags4.foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) + }) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + + awaitCond({ + router.stateData.channels == initChannels + }, max = 30 seconds, interval = 500 millis) + + + val updatedIds = shortChannelIds.drop(100).take(50) + val recentChannelUpdates = updatedIds.foldLeft(initChannelUpdates) { + case (updates, id) => + val desc = ChannelDesc(id, RoutingSyncSpec.priv1.publicKey, RoutingSyncSpec.priv2.publicKey) + val update = updates(desc) + val foo = Announcements.isNode1(update.channelFlags) + assert(foo) + val newUpdate = RoutingSyncSpec.makeNewerChannelUpdate(update) + updates.updated(desc, newUpdate) + } + // ask router to send a channel range query + sender.send(router, SendChannelQueryEx2(remoteNodeId, sender.ref)) + val QueryChannelRangeEx2(_, firstBlockNum1, numberOfBlocks1) = sender.expectMsgType[QueryChannelRangeEx2] + sender.expectMsgType[GossipTimestampFilter] + + // send back all our ids and timestamps + val List(block1) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum1, numberOfBlocks1, shortChannelIds, Router.getTimestamps(initChannels, recentChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block.firstBlock, block.numBlocks, 1, block1.shortChannelIdAndTimestamps))) + + // router should ask for our new channel updates + val QueryShortChannelIdsEx2(_, data5) = sender.expectMsgType[QueryShortChannelIdsEx2] + val (_, shortChannelIdAndFlags5) = ShortChannelIdAndFlagsBlock.decode(data5) + assert(shortChannelIdAndFlags5.keySet == updatedIds) + assert(shortChannelIdAndFlags5.values.toSet == Set(ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1)) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 78952a2c38..ea1e73c81f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -12,6 +12,7 @@ import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement import fr.acinq.eclair.wire._ import org.scalatest.FunSuiteLike +import scala.compat.Platform import scala.concurrent.duration._ @@ -112,14 +113,28 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { object RoutingSyncSpec { + val (priv_a, priv_b, priv_funding_a, priv_funding_b) = (randomKey, randomKey, randomKey, randomKey) + val (priv1, priv2) = if (Announcements.isNode1(priv_a.publicKey.toBin, priv_b.publicKey.toBin)) + (priv_a, priv_b) + else + (priv_b, priv_a) + def makeFakeRoutingInfo(shortChannelId: ShortChannelId): (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement) = { - val (priv_a, priv_b, priv_funding_a, priv_funding_b) = (randomKey, randomKey, randomKey, randomKey) + val timestamp = Platform.currentTime / 1000 val channelAnn_ab = channelAnnouncement(shortChannelId, priv_a, priv_b, priv_funding_a, priv_funding_b) val TxCoordinates(blockHeight, _, _) = ShortChannelId.coordinates(shortChannelId) - val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_b.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = blockHeight) - val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, priv_a.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = blockHeight) + val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_b.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) + val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, priv_a.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) val nodeAnnouncement_a = makeNodeAnnouncement(priv_a, "a", Alice.nodeParams.color, List()) val nodeAnnouncement_b = makeNodeAnnouncement(priv_b, "b", Bob.nodeParams.color, List()) (channelAnn_ab, channelUpdate_ab, channelUpdate_ba, nodeAnnouncement_a, nodeAnnouncement_b) } + + def makeNewerChannelUpdate(channelUpdate: ChannelUpdate) : ChannelUpdate = { + val (priv, pub) = if (Announcements.isNode1(channelUpdate.channelFlags)) (priv1, priv2.publicKey) else (priv2, priv1.publicKey) + makeChannelUpdate(channelUpdate.chainHash, priv, pub, channelUpdate.shortChannelId, + channelUpdate.cltvExpiryDelta, channelUpdate.htlcMinimumMsat, + channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, + channelUpdate.htlcMinimumMsat, Announcements.isEnabled(channelUpdate.channelFlags), channelUpdate.timestamp + 5000) + } } From a2632008321fda5b7190943acce6ee85416cf4ae Mon Sep 17 00:00:00 2001 From: sstone Date: Sun, 2 Dec 2018 15:08:57 +0100 Subject: [PATCH 17/86] Extended channel queries: clean up Use more descriptive names and specifiy what is specific to our own prototypes and what is proposed for BOLT 1.1 --- eclair-core/src/main/resources/reference.conf | 2 +- .../main/scala/fr/acinq/eclair/Features.scala | 7 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 11 ++- .../scala/fr/acinq/eclair/router/Router.scala | 89 ++++++++++-------- .../ShortChannelIdAndTimestampBlock.scala | 7 ++ .../eclair/wire/LightningMessageCodecs.scala | 48 +++++----- .../eclair/wire/LightningMessageTypes.scala | 93 ++++++++++--------- .../router/ChannelRangeQueriesSpec.scala | 10 +- ...annelRangeQueriesWithTimestampsSpec.scala} | 8 +- ...xSpec.scala => RoutingSyncProtoSpec.scala} | 16 ++-- ...la => RoutingSyncWithTimestampsSpec.scala} | 32 +++---- 11 files changed, 176 insertions(+), 147 deletions(-) rename eclair-core/src/test/scala/fr/acinq/eclair/router/{ChannelRangeQueriesExSpec.scala => ChannelRangeQueriesWithTimestampsSpec.scala} (75%) rename eclair-core/src/test/scala/fr/acinq/eclair/router/{RoutingSyncExSpec.scala => RoutingSyncProtoSpec.scala} (82%) rename eclair-core/src/test/scala/fr/acinq/eclair/router/{RouterSyncEx2Spec.scala => RoutingSyncWithTimestampsSpec.scala} (82%) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 4f91adb846..b5104ccfd4 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -42,7 +42,7 @@ eclair { node-alias = "eclair" node-color = "49daaa" global-features = "" - local-features = "808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex + local-features = "808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_proto + option_channel_range_queries_ex channel-flags = 1 // announce channels dust-limit-satoshis = 546 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 0e247c39c9..12e89ac576 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -36,8 +36,11 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 - val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 14 - val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 15 + val CHANNEL_RANGE_QUERIES_PROTO_BIT_MANDATORY = 14 + val CHANNEL_RANGE_QUERIES_PROTO_BIT_OPTIONAL = 15 + + val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 16 + val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 17 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) 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 5804ad261e..8ae1ef986b 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 @@ -100,8 +100,11 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) + val remoteHasChannelRangeQueriesProtoOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_PROTO_BIT_OPTIONAL) + val remoteHasChannelRangeQueriesProtoMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_PROTO_BIT_MANDATORY) val remoteHasChannelRangeQueriesExOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) val remoteHasChannelRangeQueriesExMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY) + log.info(s"$remoteNodeId has features: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") if (Features.areSupported(remoteInit.localFeatures)) { d.origin_opt.foreach(origin => origin ! "connected") @@ -110,6 +113,9 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { // if they support extended channel queries we do nothing, they will send us their filters log.info("{} has set initial routing sync and support extended channel range queries, we do nothing (they will send us a query)", remoteNodeId) + } else if (remoteHasChannelRangeQueriesProtoOptional || remoteHasChannelRangeQueriesProtoMandatory) { + // if they support extended channel queries we do nothing, they will send us their filters + log.info("{} has set initial routing sync and support proto channel range queries, we do nothing (they will send us a query)", remoteNodeId) } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries we do nothing, they will send us their filters log.info("{} has set initial routing sync and support channel range queries, we do nothing (they will send us a query)", remoteNodeId) @@ -120,7 +126,10 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { // if they support extended channel queries, always ask for their filter - router ! SendChannelQueryEx(remoteNodeId, d.transport) + router ! SendChannelQueryWithTimestamps(remoteNodeId, d.transport) + } else if (remoteHasChannelRangeQueriesProtoOptional || remoteHasChannelRangeQueriesProtoMandatory) { + // if they support proto channel queries, always ask for their filter + router ! SendChannelQueryProto(remoteNodeId, d.transport) } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter router ! SendChannelQuery(remoteNodeId, d.transport) 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 b9b86431d0..883f64fb55 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 @@ -55,9 +55,17 @@ case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChan } case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed) case class LiftChannelExclusion(desc: ChannelDesc) + +// channel queries as specified in BOLT 1.0 case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) -case class SendChannelQueryEx(remoteNodeId: PublicKey, to: ActorRef) -case class SendChannelQueryEx2(remoteNodeId: PublicKey, to: ActorRef) + +// channel queries with one extra timestamp, used by eclair prototypes +// remove ASAP i.e as soon as mobile apps have been updated with the new queries below +case class SendChannelQueryProto(remoteNodeId: PublicKey, to: ActorRef) + +// channel queries with 2 extra timestamps (one per chanel update), proposed for BOLT 1.1 +case class SendChannelQueryWithTimestamps(remoteNodeId: PublicKey, to: ActorRef) + case object GetRoutingState case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) @@ -93,14 +101,15 @@ case class Data(nodes: Map[PublicKey, NodeAnnouncement], object Data { /** * Process a reply_channel_range message - * @param state current state - * @param remoteNodeId id of the node that sent the reply - * @param replyChannelRangeEx2 reply_channel_range message + * + * @param state current state + * @param remoteNodeId id of the node that sent the reply + * @param replyChannelRangeEx reply_channel_range message * @return a (new state, query) pair, where query is an optional query_short_channel_ids message to be sent to be * sent back to the remote node */ - def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx2: ReplyChannelRangeEx2): (Data, Option[QueryShortChannelIdsEx2]) = { - val ReplyChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks, _, data) = replyChannelRangeEx2 + def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx: ReplyChannelRangeWithTimestamps): (Data, Option[QueryShortChannelIdsWithTimestamps]) = { + val ReplyChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks, _, data) = replyChannelRangeEx val (format, theirTimestampMap) = ShortChannelIdAndTimestampsBlock.decode(data) val theirShortChannelIds = theirTimestampMap.keySet @@ -136,7 +145,7 @@ object Data { case None => // we don't have a pending query with this peer val (slice, rest) = shortChannelIdAndFlags.splitAt(Router.SHORTID_WINDOW) - val query = QueryShortChannelIdsEx2(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) + val query = QueryShortChannelIdsWithTimestamps(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) val state1 = state.copy(sync = state.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) (state1, Some(query)) case Some(sync) => @@ -479,10 +488,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) - case Event(SendChannelQueryEx(remoteNodeId, remote), d) => + case Event(SendChannelQueryProto(remoteNodeId, remote), d) => // ask for everything - val query = QueryChannelRangeEx(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) - log.info("sending query_channel_range_ex={}", query) + val query = QueryChannelRangeProto(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + log.info("sending query_channel_range_proto={}", query) remote ! query // we also set a pass-all filter for now (we can update it later) val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) @@ -491,10 +500,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) - case Event(SendChannelQueryEx2(remoteNodeId, remote), d) => + case Event(SendChannelQueryWithTimestamps(remoteNodeId, remote), d) => // ask for everything - val query = QueryChannelRangeEx2(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) - log.info("sending query_channel_range_ex2={}", query) + val query = QueryChannelRangeWithTimestamps(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + log.info("sending query_channel_range_ex={}", query) remote ! query // we also set a pass-all filter for now (we can update it later) val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) @@ -573,25 +582,25 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct replies.foreach(reply => transport ! reply) stay - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeProto(chainHash, firstBlockNum, numberOfBlocks)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received query_channel_range_ex={}", routingMessage) + log.info("received query_channel_range_proto={}", routingMessage) // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) val blocks = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamp(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - log.info("sending back reply_channel_range_ex with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - val replies = blocks.map(block => ReplyChannelRangeEx(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + log.info("sending back reply_channel_range_proto with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + val replies = blocks.map(block => ReplyChannelRangeProto(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) replies.foreach(reply => transport ! reply) stay - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received query_channel_range_ex2={}", routingMessage) + log.info("received query_channel_range_ex={}", routingMessage) // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) val blocks = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamps(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - log.info("sending back reply_channel_range_ex2 with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - val replies = blocks.map(block => ReplyChannelRangeEx2(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + log.info("sending back reply_channel_range_ex with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + val replies = blocks.map(block => ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) replies.foreach(reply => transport ! reply) stay @@ -618,7 +627,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct context.system.eventStream.publish(syncProgress(d1)) stay using d1 - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeProto(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (format, theirTimestampMap) = ShortChannelIdAndTimestampBlock.decode(data) @@ -636,7 +645,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct val outdated = ourShortChannelIds.filter(id => { theirShortChannelIds.contains(id) && Router.getTimestamp(d.channels, d.updates)(id) < theirTimestampMap(id) }) - log.info("received reply_channel_range_ex, we're missing {} channel announcements and we have {} outdated ones, format={} ", missing.size, outdated.size, format) + log.info("received reply_channel_range_proto, we're missing {} channel announcements and we have {} outdated ones, format={} ", missing.size, outdated.size, format) // we first sync missing channels, then outdated ones val d1 = if (missing.nonEmpty) { // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not @@ -645,7 +654,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case None => // we don't have a pending query with this peer val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) + transport ! QueryShortChannelIdsProto(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size, outdated, outdated.size))) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state @@ -656,7 +665,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct case None => // we don't have a pending query with this peer val (slice, rest) = outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) + transport ! QueryShortChannelIdsProto(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet(), 0, outdated, outdated.size))) case Some(sync) => // we already have a pending query with this peer, add outdated ids to our "sync" state @@ -666,7 +675,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct context.system.eventStream.publish(syncProgress(d1)) stay using d1 - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (d1, query) = Data.handleReplyChannelRange(d, remoteNodeId, routingMessage) query.map(q => transport ! q) @@ -691,10 +700,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct stay // extended query message: a flag and a list of channel ids - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsEx(chainHash, flag, data)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsProto(chainHash, flag, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (_, shortChannelIds, _) = ShortChannelIdsBlock.decode(data) - log.info("received query_short_channel_ids_ex for {} channel announcements, flag={}", shortChannelIds.size, flag) + log.info("received query_short_channel_ids_proto for {} channel announcements, flag={}", shortChannelIds.size, flag) shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) @@ -704,15 +713,15 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } }) - transport ! ReplyShortChannelIdsEndEx(chainHash, 1) + transport ! ReplyShortChannelIdsEndProto(chainHash, 1) stay // new extended query message: a list of [channel id + flag] - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsEx2(chainHash, data)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsWithTimestamps(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (_, shortChannelIdAndFlags) = ShortChannelIdAndFlagsBlock.decode(data) - log.info("received query_short_channel_ids_ex2 for {} channel announcements", shortChannelIdAndFlags.size) + log.info("received query_short_channel_ids_ex for {} channel announcements", shortChannelIdAndFlags.size) shortChannelIdAndFlags.foreach { case (shortChannelId, flag) => { d.channels.get(shortChannelId) match { @@ -726,7 +735,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct } } } - transport ! ReplyShortChannelIdsEndEx2(chainHash, 1) + transport ! ReplyShortChannelIdsEndWithTimestamps(chainHash, 1) stay case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEnd(chainHash, complete)), d) => @@ -749,20 +758,20 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct context.system.eventStream.publish(syncProgress(d1)) stay using d1 - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndEx(chainHash, complete)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndProto(chainHash, complete)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received reply_short_channel_ids_end_ex={}", routingMessage) + log.info("received reply_short_channel_ids_end_proto={}", routingMessage) // have we more channels to ask this peer? val d1 = d.sync.get(remoteNodeId) match { case Some(sync) if sync.missing.nonEmpty => log.info(s"asking {} for the next slice of missing short_channel_ids", remoteNodeId) val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIdsProto(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) case Some(sync) if sync.outdated.nonEmpty => log.info(s"asking {} for the next slice of outdated short_channel_ids", remoteNodeId) val (slice, rest) = sync.outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIdsProto(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = rest))) case Some(sync) if sync.missing.isEmpty && sync.outdated.isEmpty => // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove @@ -774,15 +783,15 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct context.system.eventStream.publish(syncProgress(d1)) stay using d1 - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndEx2(chainHash, complete)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndWithTimestamps(chainHash, complete)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received reply_short_channel_ids_end_ex2={}", routingMessage) + log.info("received reply_short_channel_ids_end_ex={}", routingMessage) // have we more channels to ask this peer? val d1 = d.sync.get(remoteNodeId) match { case Some(sync) if sync.outdatedEx.nonEmpty => log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) val (slice, rest) = sync.outdatedEx.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx2(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT)) + transport ! QueryShortChannelIdsWithTimestamps(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdatedEx = rest))) case Some(sync) if sync.outdatedEx.isEmpty => // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala index 8d2ed21016..14fafd0d57 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala @@ -12,6 +12,13 @@ import scala.annotation.tailrec import scala.collection.SortedSet import scala.collection.immutable.SortedMap +/** + * + * README: this is an obsolete design for channel range queries, used by eclair only, and to be dropped ASAP + * @param firstBlock fist block covered by this message + * @param numBlocks number of blocks covered by this message + * @param shortChannelIdAndTimestamps packed list of short channel id + the most recent channel update timestamp + */ case class ShortChannelIdAndTimestampBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: BinaryData) object ShortChannelIdAndTimestampBlock { 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 8cdb387df2..676c6982fb 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 @@ -321,54 +321,54 @@ object LightningMessageCodecs { ("data" | varsizebinarydata) ).as[ReplyChannelRange] - val queryShortChannelIdsExCodec: Codec[QueryShortChannelIdsEx] = ( + val queryShortChannelIdsProtoCodec: Codec[QueryShortChannelIdsProto] = ( ("chainHash" | binarydata(32)) :: ("flag" | byte) :: ("data" | varsizebinarydata) - ).as[QueryShortChannelIdsEx] + ).as[QueryShortChannelIdsProto] - val replyShortChanelIdsEndExCodec: Codec[ReplyShortChannelIdsEndEx] = ( + val replyShortChanelIdsEndProtoCodec: Codec[ReplyShortChannelIdsEndProto] = ( ("chainHash" | binarydata(32)) :: ("complete" | byte) - ).as[ReplyShortChannelIdsEndEx] + ).as[ReplyShortChannelIdsEndProto] - val queryChannelRangeExCodec: Codec[QueryChannelRangeEx] = ( + val queryChannelRangeProtoCodec: Codec[QueryChannelRangeProto] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) - ).as[QueryChannelRangeEx] + ).as[QueryChannelRangeProto] - val replyChannelRangeExCodec: Codec[ReplyChannelRangeEx] = ( + val replyChannelRangeProtoCodec: Codec[ReplyChannelRangeProto] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: ("complete" | byte) :: ("data" | varsizebinarydata) - ).as[ReplyChannelRangeEx] + ).as[ReplyChannelRangeProto] - val queryShortChannelIdsEx2Codec: Codec[QueryShortChannelIdsEx2] = ( + val queryShortChannelIdsWithTimestampsCodec: Codec[QueryShortChannelIdsWithTimestamps] = ( ("chainHash" | binarydata(32)) :: ("data" | varsizebinarydata) - ).as[QueryShortChannelIdsEx2] + ).as[QueryShortChannelIdsWithTimestamps] - val replyShortChanelIdsEndEx2Codec: Codec[ReplyShortChannelIdsEndEx2] = ( + val replyShortChanelIdsEndWithTimestampsCodec: Codec[ReplyShortChannelIdsEndWithTimestamps] = ( ("chainHash" | binarydata(32)) :: ("complete" | byte) - ).as[ReplyShortChannelIdsEndEx2] + ).as[ReplyShortChannelIdsEndWithTimestamps] - val queryChannelRangeEx2Codec: Codec[QueryChannelRangeEx2] = ( + val queryChannelRangeWithTimestampsCodec: Codec[QueryChannelRangeWithTimestamps] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) - ).as[QueryChannelRangeEx2] + ).as[QueryChannelRangeWithTimestamps] - val replyChannelRangeEx2Codec: Codec[ReplyChannelRangeEx2] = ( + val replyChannelRangeWithTimestampsCodec: Codec[ReplyChannelRangeWithTimestamps] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: ("complete" | byte) :: ("data" | varsizebinarydata) - ).as[ReplyChannelRangeEx2] + ).as[ReplyChannelRangeWithTimestamps] val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( ("chainHash" | binarydata(32)) :: @@ -405,14 +405,14 @@ object LightningMessageCodecs { .typecase(263, queryChannelRangeCodec) .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) - .typecase(1001, queryShortChannelIdsExCodec) - .typecase(1002, replyShortChanelIdsEndExCodec) - .typecase(1003, queryChannelRangeExCodec) - .typecase(1004, replyChannelRangeExCodec) - .typecase(1011, queryShortChannelIdsEx2Codec) - .typecase(1012, replyShortChanelIdsEndEx2Codec) - .typecase(1013, queryChannelRangeEx2Codec) - .typecase(1014, replyChannelRangeEx2Codec) + .typecase(1001, queryShortChannelIdsProtoCodec) + .typecase(1002, replyShortChanelIdsEndProtoCodec) + .typecase(1003, queryChannelRangeProtoCodec) + .typecase(1004, replyChannelRangeProtoCodec) + .typecase(1011, queryShortChannelIdsWithTimestampsCodec) + .typecase(1012, replyShortChanelIdsEndWithTimestampsCodec) + .typecase(1013, queryChannelRangeWithTimestampsCodec) + .typecase(1014, replyChannelRangeWithTimestampsCodec) /** 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 22efc7c39f..2eb49ce3d4 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 @@ -207,6 +207,8 @@ case class PerHopPayload(shortChannelId: ShortChannelId, amtToForward: Long, outgoingCltvValue: Long) +// BOLT 1.0 channel queries + /** * * @param chainHash chain hash @@ -215,43 +217,38 @@ case class PerHopPayload(shortChannelId: ShortChannelId, case class QueryShortChannelIds(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash -/** - * - * @param chainHash chain hash - * @param flag if flag == 1, don't send back channel announcements - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded - */ -case class QueryShortChannelIdsEx(chainHash: BinaryData, - flag: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash - -case class QueryShortChannelIdsEx2(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash - case class QueryChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash -case class QueryChannelRangeEx(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash +case class ReplyChannelRange(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash + +case class ReplyShortChannelIdsEnd(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash + +case class GossipTimestampFilter(chainHash: BinaryData, + firstTimestamp: Long, + timestampRange: Long) extends RoutingMessage with HasChainHash -case class QueryChannelRangeEx2(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash +// prototype queries, used by eclair only, to be removed asap /** * * @param chainHash chain hash - * @param firstBlockNum first block that is found in data - * @param numberOfBlocks number of blocks spanned by data - * @param complete + * @param flag if flag == 1, don't send back channel announcements * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded */ -case class ReplyChannelRange(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash +case class QueryShortChannelIdsProto(chainHash: BinaryData, + flag: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash + +case class QueryChannelRangeProto(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long) extends RoutingMessage with HasChainHash /** * @@ -261,27 +258,31 @@ case class ReplyChannelRange(chainHash: BinaryData, * @param complete * @param data prefix + list of (short channel id + timestamp) values, where prefix specifies how the list is encoded */ -case class ReplyChannelRangeEx(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash - -case class ReplyChannelRangeEx2(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash +case class ReplyChannelRangeProto(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash -case class ReplyShortChannelIdsEnd(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash +case class ReplyShortChannelIdsEndProto(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash -case class ReplyShortChannelIdsEndEx(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash +// proposal for BOLT 1.1 channel queries -case class ReplyShortChannelIdsEndEx2(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash +case class QueryShortChannelIdsWithTimestamps(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash -case class GossipTimestampFilter(chainHash: BinaryData, - firstTimestamp: Long, - timestampRange: Long) extends RoutingMessage with HasChainHash \ No newline at end of file + + +case class QueryChannelRangeWithTimestamps(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long) extends RoutingMessage with HasChainHash + + +case class ReplyChannelRangeWithTimestamps(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash + +case class ReplyShortChannelIdsEndWithTimestamps(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index aca9bf7b9c..89c9f59dfa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Block import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.{ReplyChannelRange, ReplyChannelRangeEx} +import fr.acinq.eclair.wire.{ReplyChannelRange, ReplyChannelRangeProto} import org.scalatest.FunSuite import scala.collection.immutable.SortedMap @@ -60,7 +60,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex` messages (uncompressed format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -69,7 +69,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex` messages (zlib format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -78,7 +78,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex2` messages (uncompressed format)") { val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -87,7 +87,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex2` messages (zlib format)") { val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala similarity index 75% rename from eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala index 4f7f7f27fc..8232629fd2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala @@ -2,20 +2,20 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Block import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.ReplyChannelRangeEx +import fr.acinq.eclair.wire.ReplyChannelRangeProto import org.scalatest.FunSuite import scala.collection.immutable.SortedMap import scala.util.Random -class ChannelRangeQueriesExSpec extends FunSuite { +class ChannelRangeQueriesWithTimestampsSpec extends FunSuite { val random = new Random() val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds val timestamps = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap test("create `reply_channel_range_ex` messages (uncompressed format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -24,7 +24,7 @@ class ChannelRangeQueriesExSpec extends FunSuite { test("create `reply_channel_range_ex` messages (zlib format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala similarity index 82% rename from eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala index e43277eab8..1e5aabacb1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala @@ -12,7 +12,7 @@ import scala.collection.immutable.TreeMap import scala.concurrent.duration._ -class RoutingSyncExSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { +class RoutingSyncProtoSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { import RoutingSyncSpec.makeFakeRoutingInfo test("handle chanel range extended queries") { @@ -23,8 +23,8 @@ class RoutingSyncExSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val remoteNodeId = TestConstants.Bob.nodeParams.nodeId // ask router to send a channel range query - sender.send(router, SendChannelQueryEx(remoteNodeId, sender.ref)) - val QueryChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeEx] + sender.send(router, SendChannelQueryProto(remoteNodeId, sender.ref)) + val QueryChannelRangeProto(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeProto] sender.expectMsgType[GossipTimestampFilter] @@ -42,14 +42,14 @@ class RoutingSyncExSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val List(block3) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) // send first block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeProto(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) // router should ask for our first block of ids - val QueryShortChannelIdsEx(_, _, data1) = sender.expectMsgType[QueryShortChannelIdsEx] + val QueryShortChannelIdsProto(_, _, data1) = sender.expectMsgType[QueryShortChannelIdsProto] val (_, shortChannelIds1, false) = ShortChannelIdsBlock.decode(data1) assert(shortChannelIds1 == shortChannelIds.take(100)) // send second block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeProto(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) // router should not ask for more ids, it already has a pending query ! sender.expectNoMsg(1 second) @@ -73,10 +73,10 @@ class RoutingSyncExSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.expectNoMsg(1 second) // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndProto(chainHash, 1.toByte))) // router should ask for our second block of ids - val QueryShortChannelIdsEx(_, _, data2) = sender.expectMsgType[QueryShortChannelIdsEx] + val QueryShortChannelIdsProto(_, _, data2) = sender.expectMsgType[QueryShortChannelIdsProto] val (_, shortChannelIds2, false) = ShortChannelIdsBlock.decode(data2) assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala similarity index 82% rename from eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala index a700e3bd9d..ea68a290a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSyncEx2Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala @@ -16,7 +16,7 @@ import scala.collection.immutable.TreeMap import scala.concurrent.duration._ -class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike { +class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { import RoutingSyncSpec.makeFakeRoutingInfo @@ -51,16 +51,16 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike val remoteNodeId = TestConstants.Bob.nodeParams.nodeId // ask router to send a channel range query - sender.send(router, SendChannelQueryEx2(remoteNodeId, sender.ref)) - val QueryChannelRangeEx2(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeEx2] + sender.send(router, SendChannelQueryWithTimestamps(remoteNodeId, sender.ref)) + val QueryChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeWithTimestamps] sender.expectMsgType[GossipTimestampFilter] // send back all our ids and timestamps val List(block) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps))) // router should ask for our first block of ids - val QueryShortChannelIdsEx2(_, data1) = sender.expectMsgType[QueryShortChannelIdsEx2] + val QueryShortChannelIdsWithTimestamps(_, data1) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] val (_, shortChannelIdAndFlags1) = ShortChannelIdAndFlagsBlock.decode(data1) assert(shortChannelIdAndFlags1.keySet == shortChannelIds.take(Router.SHORTID_WINDOW)) assert(shortChannelIdAndFlags1.values.toSet == Set(ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) @@ -84,10 +84,10 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.expectNoMsg(1 second) // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) // router should ask for our second block of ids - val QueryShortChannelIdsEx2(_, data2) = sender.expectMsgType[QueryShortChannelIdsEx2] + val QueryShortChannelIdsWithTimestamps(_, data2) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] val (_, shortChannelIdAndFlags2) = ShortChannelIdAndFlagsBlock.decode(data2) assert(shortChannelIdAndFlags2.keySet == shortChannelIds.drop(Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) @@ -98,10 +98,10 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) // router should ask for our third block of ids - val QueryShortChannelIdsEx2(_, data3) = sender.expectMsgType[QueryShortChannelIdsEx2] + val QueryShortChannelIdsWithTimestamps(_, data3) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] val (_, shortChannelIdAndFlags3) = ShortChannelIdAndFlagsBlock.decode(data3) assert(shortChannelIdAndFlags3.keySet == shortChannelIds.drop(2 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) @@ -112,10 +112,10 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) // router should ask for our third block of ids - val QueryShortChannelIdsEx2(_, data4) = sender.expectMsgType[QueryShortChannelIdsEx2] + val QueryShortChannelIdsWithTimestamps(_, data4) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] val (_, shortChannelIdAndFlags4) = ShortChannelIdAndFlagsBlock.decode(data4) assert(shortChannelIdAndFlags4.keySet == shortChannelIds.drop(3 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) // send block #4 @@ -125,7 +125,7 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx2(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) awaitCond({ router.stateData.channels == initChannels @@ -143,16 +143,16 @@ class RoutingSyncEx2Spec extends TestKit(ActorSystem("test")) with FunSuiteLike updates.updated(desc, newUpdate) } // ask router to send a channel range query - sender.send(router, SendChannelQueryEx2(remoteNodeId, sender.ref)) - val QueryChannelRangeEx2(_, firstBlockNum1, numberOfBlocks1) = sender.expectMsgType[QueryChannelRangeEx2] + sender.send(router, SendChannelQueryWithTimestamps(remoteNodeId, sender.ref)) + val QueryChannelRangeWithTimestamps(_, firstBlockNum1, numberOfBlocks1) = sender.expectMsgType[QueryChannelRangeWithTimestamps] sender.expectMsgType[GossipTimestampFilter] // send back all our ids and timestamps val List(block1) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum1, numberOfBlocks1, shortChannelIds, Router.getTimestamps(initChannels, recentChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx2(chainHash, block.firstBlock, block.numBlocks, 1, block1.shortChannelIdAndTimestamps))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block1.shortChannelIdAndTimestamps))) // router should ask for our new channel updates - val QueryShortChannelIdsEx2(_, data5) = sender.expectMsgType[QueryShortChannelIdsEx2] + val QueryShortChannelIdsWithTimestamps(_, data5) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] val (_, shortChannelIdAndFlags5) = ShortChannelIdAndFlagsBlock.decode(data5) assert(shortChannelIdAndFlags5.keySet == updatedIds) assert(shortChannelIdAndFlags5.values.toSet == Set(ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1)) From e17ef259e83a11777ae90bdff5403bc193254a10 Mon Sep 17 00:00:00 2001 From: pm47 Date: Sun, 20 Jan 2019 19:28:11 +0100 Subject: [PATCH 18/86] added checksum prototype --- .../main/scala/fr/acinq/eclair/Features.scala | 7 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 20 ++- .../scala/fr/acinq/eclair/router/Router.scala | 153 +++++++++++++++--- .../eclair/wire/LightningMessageCodecs.scala | 119 +++++++++----- .../eclair/wire/LightningMessageTypes.scala | 80 +++++---- .../router/ChannelRangeQueriesSpec.scala | 10 +- ...hannelRangeQueriesWithTimestampsSpec.scala | 6 +- .../eclair/router/RoutingSyncProtoSpec.scala | 14 +- .../RoutingSyncWithTimestampsSpec.scala | 18 +-- .../wire/LightningMessageCodecsSpec.scala | 69 +++++++- 10 files changed, 372 insertions(+), 124 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 12e89ac576..c3b5f5d45e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -36,12 +36,15 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 - val CHANNEL_RANGE_QUERIES_PROTO_BIT_MANDATORY = 14 - val CHANNEL_RANGE_QUERIES_PROTO_BIT_OPTIONAL = 15 + val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_MANDATORY = 14 + val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_OPTIONAL = 15 val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 16 val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 17 + val CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_MANDATORY = 18 + val CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_OPTIONAL = 19 + def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) def hasFeature(features: BinaryData, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit) 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 5bb81247d4..b586c91d94 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 @@ -104,20 +104,25 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) - val remoteHasChannelRangeQueriesProtoOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_PROTO_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesProtoMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_PROTO_BIT_MANDATORY) + val remoteHasChannelRangeQueriesDeprecatedOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_OPTIONAL) + val remoteHasChannelRangeQueriesDeprecatedMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_MANDATORY) val remoteHasChannelRangeQueriesExOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) val remoteHasChannelRangeQueriesExMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY) + val remoteHasChannelRangeQueriesChecksumOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_MANDATORY) + val remoteHasChannelRangeQueriesChecksumMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_OPTIONAL) log.info(s"$remoteNodeId has features: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") if (Features.areSupported(remoteInit.localFeatures)) { d.origin_opt.foreach(origin => origin ! "connected") if (remoteHasInitialRoutingSync) { - if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { + if (remoteHasChannelRangeQueriesChecksumOptional || remoteHasChannelRangeQueriesChecksumMandatory) { + // if they support extended channel queries we do nothing, they will send us their filters + log.info("{} has set initial routing sync and support checksum channel range queries, we do nothing (they will send us a query)", remoteNodeId) + } else if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { // if they support extended channel queries we do nothing, they will send us their filters log.info("{} has set initial routing sync and support extended channel range queries, we do nothing (they will send us a query)", remoteNodeId) - } else if (remoteHasChannelRangeQueriesProtoOptional || remoteHasChannelRangeQueriesProtoMandatory) { + } else if (remoteHasChannelRangeQueriesDeprecatedOptional || remoteHasChannelRangeQueriesDeprecatedMandatory) { // if they support extended channel queries we do nothing, they will send us their filters log.info("{} has set initial routing sync and support proto channel range queries, we do nothing (they will send us a query)", remoteNodeId) } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { @@ -129,11 +134,14 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } } if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { + // if they support extended channel queries, always ask for their filter + router ! SendChannelQueryWithChecksums(remoteNodeId, d.transport) + } else if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { // if they support extended channel queries, always ask for their filter router ! SendChannelQueryWithTimestamps(remoteNodeId, d.transport) - } else if (remoteHasChannelRangeQueriesProtoOptional || remoteHasChannelRangeQueriesProtoMandatory) { + } else if (remoteHasChannelRangeQueriesDeprecatedOptional || remoteHasChannelRangeQueriesDeprecatedMandatory) { // if they support proto channel queries, always ask for their filter - router ! SendChannelQueryProto(remoteNodeId, d.transport) + router ! SendChannelQueryDeprecated(remoteNodeId, d.transport) } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter router ! SendChannelQuery(remoteNodeId, d.transport) 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 e83b2a3e2e..01c2ac21d0 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 @@ -16,6 +16,8 @@ package fr.acinq.eclair.router +import java.util.zip.Adler32 + import akka.actor.{ActorRef, Props, Status} import akka.event.Logging.MDC import akka.pattern.pipe @@ -31,6 +33,8 @@ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ +import shapeless.HNil + import scala.collection.{SortedSet, mutable} import scala.collection.immutable.{SortedMap, TreeMap} import scala.compat.Platform @@ -54,11 +58,14 @@ case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) // channel queries with one extra timestamp, used by eclair prototypes // remove ASAP i.e as soon as mobile apps have been updated with the new queries below -case class SendChannelQueryProto(remoteNodeId: PublicKey, to: ActorRef) +case class SendChannelQueryDeprecated(remoteNodeId: PublicKey, to: ActorRef) // channel queries with 2 extra timestamps (one per chanel update), proposed for BOLT 1.1 case class SendChannelQueryWithTimestamps(remoteNodeId: PublicKey, to: ActorRef) +// channel queries with 2 extra timestamps (one per chanel update) and a checksum +case class SendChannelQueryWithChecksums(remoteNodeId: PublicKey, to: ActorRef) + case object GetRoutingState case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) @@ -66,7 +73,7 @@ case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], update case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int, outdated: SortedSet[ShortChannelId] = SortedSet.empty[ShortChannelId], totalOutdatedCount: Int = 0, - outdatedEx :SortedSet[ShortChannelIdAndFlag] = SortedSet.empty[ShortChannelIdAndFlag], totalOutdatedExCount: Int = 0) { + outdatedEx: SortedSet[ShortChannelIdAndFlag] = SortedSet.empty[ShortChannelIdAndFlag], totalOutdatedExCount: Int = 0) { // returns a sync progress indicator (1 means fully synced) def progress: Double = { val inflight = (missing.size + outdated.size + outdatedEx.size) * 1.0 @@ -99,7 +106,7 @@ object Data { * @return a (new state, query) pair, where query is an optional query_short_channel_ids message to be sent to be * sent back to the remote node */ - def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx: ReplyChannelRangeWithTimestamps): (Data, Option[QueryShortChannelIdsWithTimestamps]) = { + def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx: ReplyChannelRangeWithTimestamps): (Data, Option[QueryShortChannelIdsWithFlags]) = { val ReplyChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks, _, data) = replyChannelRangeEx val (format, theirTimestampMap) = ShortChannelIdAndTimestampsBlock.decode(data) val theirShortChannelIds = theirTimestampMap.keySet @@ -108,7 +115,7 @@ object Data { val ourShortChannelIds: SortedSet[ShortChannelId] = state.channels.keySet.filter(id => { val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) - }) + }) // TODO: this doesn't work if there is overlap in the blocks // missing are the ones we don't have val missing = (theirShortChannelIds -- ourShortChannelIds).map(id => ShortChannelIdAndFlag(id, ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) @@ -136,7 +143,45 @@ object Data { case None => // we don't have a pending query with this peer val (slice, rest) = shortChannelIdAndFlags.splitAt(Router.SHORTID_WINDOW) - val query = QueryShortChannelIdsWithTimestamps(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) + val query = QueryShortChannelIdsWithFlags(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) + val state1 = state.copy(sync = state.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) + (state1, Some(query)) + case Some(sync) => + // we already have a pending query with this peer, add missing ids to our "sync" state + val state1 = state.copy(sync = state.sync + (remoteNodeId -> sync.copy(outdatedEx = sync.outdatedEx ++ shortChannelIdAndFlags, totalOutdatedExCount = sync.totalOutdatedExCount + shortChannelIdAndFlags.size))) + (state1, None) + } + } else (state, None) // nothing to do + + (newState, query) + } + + def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx: ReplyChannelRangeWithChecksums): (Data, Option[QueryShortChannelIdsWithFlags]) = { + val shortChannelIdAndFlagsList = replyChannelRangeEx + .data + .array + .map { channelInfo => + var flag = 0 + if (state.channels.contains(channelInfo.shortChannelId)) { + val ourInfo = Router.getChannelDigestInfo(state.channels, state.updates)(channelInfo.shortChannelId) + if (ourInfo.timestamp1 < channelInfo.timestamp1 && ourInfo.checksum1 != channelInfo.checksum1) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 + if (ourInfo.timestamp2 < channelInfo.timestamp2 && ourInfo.checksum2 != channelInfo.checksum2) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 + } else { + // we don't know this channel: we request everything + flag = flag | ChannelRangeQueries.INCLUDE_ANNOUNCEMENT | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 + } + ShortChannelIdAndFlag(channelInfo.shortChannelId, flag.toByte) + } + .filter(_.flag != 0) + + val shortChannelIdAndFlags = collection.SortedSet(shortChannelIdAndFlagsList: _*) + + val (newState, query) = if (shortChannelIdAndFlags.nonEmpty) { + state.sync.get(remoteNodeId) match { + case None => + // we don't have a pending query with this peer + val (slice, rest) = shortChannelIdAndFlags.splitAt(Router.SHORTID_WINDOW) + val query = QueryShortChannelIdsWithFlags(replyChannelRangeEx.chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, replyChannelRangeEx.data.encoding)) val state1 = state.copy(sync = state.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) (state1, Some(query)) case Some(sync) => @@ -408,7 +453,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } val staleChannelsToRemove = new mutable.MutableList[ChannelDesc] - staleChannels.map(d.channels).foreach( ca => { + staleChannels.map(d.channels).foreach(ca => { staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2) staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1) }) @@ -478,9 +523,9 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) - case Event(SendChannelQueryProto(remoteNodeId, remote), d) => + case Event(SendChannelQueryDeprecated(remoteNodeId, remote), d) => // ask for everything - val query = QueryChannelRangeProto(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + val query = QueryChannelRangeDeprecated(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) log.info("sending query_channel_range_proto={}", query) remote ! query // we also set a pass-all filter for now (we can update it later) @@ -502,6 +547,18 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) + case Event(SendChannelQueryWithChecksums(remoteNodeId, remote), d) => + // ask for everything + val query = QueryChannelRangeWithChecksums(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + log.info("sending query_channel_range_with_checksums={}", query) + remote ! query + // we also set a pass-all filter for now (we can update it later) + val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) + remote ! filter + // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and + // will start a new complete sync process + stay using d.copy(sync = d.sync - remoteNodeId) + // Warning: order matters here, this must be the first match for HasChainHash messages ! case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), d) if routingMessage.chainHash != nodeParams.chainHash => sender ! TransportHandler.ReadAck(routingMessage) @@ -572,14 +629,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom replies.foreach(reply => transport ! reply) stay - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeProto(chainHash, firstBlockNum, numberOfBlocks)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks)), d) => sender ! TransportHandler.ReadAck(routingMessage) log.info("received query_channel_range_proto={}", routingMessage) // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) val blocks = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamp(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) log.info("sending back reply_channel_range_proto with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - val replies = blocks.map(block => ReplyChannelRangeProto(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeDeprecated(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) replies.foreach(reply => transport ! reply) stay @@ -594,6 +651,26 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom replies.foreach(reply => transport ! reply) stay + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + log.info("received query_channel_range_with_checksums={}", routingMessage) + // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] + val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val replies = shortChannelIds + .toList + .map(getChannelDigestInfo(d.channels, d.updates)) + .grouped(2000) // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + .map { group => + // TODO: check if the default value is correct + val firstBlock: Long = group.headOption.map(i => ShortChannelId.coordinates(i.shortChannelId).blockHeight.toLong).getOrElse(firstBlockNum) + val numBlocks: Long = group.lastOption.map(i => ShortChannelId.coordinates(i.shortChannelId).blockHeight.toLong).getOrElse(numberOfBlocks) + // complete = 1 because we maintain complete information for this chain + ReplyChannelRangeWithChecksums(chainHash, firstBlock, numBlocks, complete = 1, ChannelRangeWithChecksumsData(EncodingTypes.UNCOMPRESSED, group)) + } + log.info("sending back reply_channel_range_with_checksums with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + replies.foreach(reply => transport ! reply) + stay + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (format, theirShortChannelIds, useGzip) = ShortChannelIdsBlock.decode(data) @@ -617,7 +694,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom context.system.eventStream.publish(syncProgress(d1)) stay using d1 - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeProto(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (format, theirTimestampMap) = ShortChannelIdAndTimestampBlock.decode(data) @@ -644,7 +721,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case None => // we don't have a pending query with this peer val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsProto(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) + transport ! QueryShortChannelIdsDeprecated(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size, outdated, outdated.size))) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state @@ -655,7 +732,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case None => // we don't have a pending query with this peer val (slice, rest) = outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsProto(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) + transport ! QueryShortChannelIdsDeprecated(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet(), 0, outdated, outdated.size))) case Some(sync) => // we already have a pending query with this peer, add outdated ids to our "sync" state @@ -672,7 +749,17 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom context.system.eventStream.publish(syncProgress(d1)) stay using d1 - // standard query message: a list of channel ids + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + sender ! TransportHandler.ReadAck(routingMessage) + val (d1, query) = Data.handleReplyChannelRange(d, remoteNodeId, routingMessage) + d1.sync.get(remoteNodeId).map {sync => + log.info(s"sync.totalOutdatedExCount=${sync.totalOutdatedExCount}") + } + query.map(q => transport ! q) + context.system.eventStream.publish(syncProgress(d1)) + stay using d1 + + // standard query message: a list of channel ids case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (_, shortChannelIds, useGzip) = ShortChannelIdsBlock.decode(data) @@ -690,7 +777,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom stay // extended query message: a flag and a list of channel ids - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsProto(chainHash, flag, data)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsDeprecated(chainHash, flag, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (_, shortChannelIds, _) = ShortChannelIdsBlock.decode(data) log.info("received query_short_channel_ids_proto for {} channel announcements, flag={}", shortChannelIds.size, flag) @@ -703,12 +790,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } }) - transport ! ReplyShortChannelIdsEndProto(chainHash, 1) + transport ! ReplyShortChannelIdsEndDeprecated(chainHash, 1) stay // new extended query message: a list of [channel id + flag] - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsWithTimestamps(chainHash, data)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsWithFlags(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (_, shortChannelIdAndFlags) = ShortChannelIdAndFlagsBlock.decode(data) log.info("received query_short_channel_ids_ex for {} channel announcements", shortChannelIdAndFlags.size) @@ -725,7 +812,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } } } - transport ! ReplyShortChannelIdsEndWithTimestamps(chainHash, 1) + transport ! ReplyShortChannelIdsWithFlagsEnd(chainHash, 1) stay case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEnd(chainHash, complete)), d) => @@ -748,7 +835,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom context.system.eventStream.publish(syncProgress(d1)) stay using d1 - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndProto(chainHash, complete)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndDeprecated(chainHash, complete)), d) => sender ! TransportHandler.ReadAck(routingMessage) log.info("received reply_short_channel_ids_end_proto={}", routingMessage) // have we more channels to ask this peer? @@ -756,12 +843,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Some(sync) if sync.missing.nonEmpty => log.info(s"asking {} for the next slice of missing short_channel_ids", remoteNodeId) val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsProto(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIdsDeprecated(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) case Some(sync) if sync.outdated.nonEmpty => log.info(s"asking {} for the next slice of outdated short_channel_ids", remoteNodeId) val (slice, rest) = sync.outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsProto(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIdsDeprecated(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = rest))) case Some(sync) if sync.missing.isEmpty && sync.outdated.isEmpty => // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove @@ -773,7 +860,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom context.system.eventStream.publish(syncProgress(d1)) stay using d1 - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndWithTimestamps(chainHash, complete)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsWithFlagsEnd(chainHash, complete)), d) => sender ! TransportHandler.ReadAck(routingMessage) log.info("received reply_short_channel_ids_end_ex={}", routingMessage) // have we more channels to ask this peer? @@ -781,7 +868,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Some(sync) if sync.outdatedEx.nonEmpty => log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) val (slice, rest) = sync.outdatedEx.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsWithTimestamps(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT)) + transport ! QueryShortChannelIdsWithFlags(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT)) d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdatedEx = rest))) case Some(sync) if sync.outdatedEx.isEmpty => // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove @@ -1064,6 +1151,7 @@ object Router { val opt1 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)) val opt2 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)) val timestamps = (opt1, opt2) match { + // TODO: ??? this needs to be checked case (Some(u1), Some(u2)) if Announcements.isNode1(u1.channelFlags) && !Announcements.isNode1(u2.channelFlags) => (u1.timestamp, u2.timestamp) case (Some(u1), Some(u2)) if !Announcements.isNode1(u1.channelFlags) && Announcements.isNode1(u2.channelFlags) => (u2.timestamp, u1.timestamp) case (Some(u1), None) if Announcements.isNode1(u1.channelFlags) => (u1.timestamp, 0L) @@ -1075,6 +1163,25 @@ object Router { timestamps } + def getChannelDigestInfo(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId): ChannelDigestInfo = { + val c = channels(shortChannelId) + val u1_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) + val u2_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) + val timestamp1 = u1_opt.map(_.timestamp).getOrElse(0L) + val timestamp2 = u2_opt.map(_.timestamp).getOrElse(0L) + val checksum1 = u1_opt.map(getChecksum).getOrElse(0L) + val checksum2 = u2_opt.map(getChecksum).getOrElse(0L) + ChannelDigestInfo(shortChannelId, timestamp1, timestamp2, checksum1, checksum2) + } + + def getChecksum(u: ChannelUpdate): Long = { + import u._ + val data = serializationResult(LightningMessageCodecs.channelUpdateChecksumCodec.encode(shortChannelId :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: HNil)) + val checksum = new Adler32() + checksum.update(data.data.toArray) + checksum.getValue + } + /** * https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications */ 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 676c6982fb..dd3953935b 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 @@ -280,6 +280,17 @@ object LightningMessageCodecs { ("signature" | signature) :: nodeAnnouncementWitnessCodec).as[NodeAnnouncement] + val channelUpdateChecksumCodec = + ("shortChannelId" | shortchannelid) :: + (("messageFlags" | byte) >>:~ { messageFlags => + ("channelFlags" | byte) :: + ("cltvExpiryDelta" | uint16) :: + ("htlcMinimumMsat" | uint64) :: + ("feeBaseMsat" | uint32) :: + ("feeProportionalMillionths" | uint32) :: + ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, uint64)) + }) + val channelUpdateWitnessCodec = ("chainHash" | binarydata(32)) :: ("shortChannelId" | shortchannelid) :: @@ -321,40 +332,46 @@ object LightningMessageCodecs { ("data" | varsizebinarydata) ).as[ReplyChannelRange] - val queryShortChannelIdsProtoCodec: Codec[QueryShortChannelIdsProto] = ( + val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( + ("chainHash" | binarydata(32)) :: + ("firstTimestamp" | uint32) :: + ("timestampRange" | uint32) + ).as[GossipTimestampFilter] + + val queryShortChannelIdsDeprecatedCodec: Codec[QueryShortChannelIdsDeprecated] = ( ("chainHash" | binarydata(32)) :: ("flag" | byte) :: ("data" | varsizebinarydata) - ).as[QueryShortChannelIdsProto] - - val replyShortChanelIdsEndProtoCodec: Codec[ReplyShortChannelIdsEndProto] = ( - ("chainHash" | binarydata(32)) :: - ("complete" | byte) - ).as[ReplyShortChannelIdsEndProto] - - val queryChannelRangeProtoCodec: Codec[QueryChannelRangeProto] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) - ).as[QueryChannelRangeProto] - - val replyChannelRangeProtoCodec: Codec[ReplyChannelRangeProto] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) :: - ("complete" | byte) :: - ("data" | varsizebinarydata) - ).as[ReplyChannelRangeProto] - - val queryShortChannelIdsWithTimestampsCodec: Codec[QueryShortChannelIdsWithTimestamps] = ( - ("chainHash" | binarydata(32)) :: - ("data" | varsizebinarydata) - ).as[QueryShortChannelIdsWithTimestamps] - - val replyShortChanelIdsEndWithTimestampsCodec: Codec[ReplyShortChannelIdsEndWithTimestamps] = ( + ).as[QueryShortChannelIdsDeprecated] + + val replyShortChanelIdsEndDeprecatedCodec: Codec[ReplyShortChannelIdsEndDeprecated] = ( + ("chainHash" | binarydata(32)) :: + ("complete" | byte) + ).as[ReplyShortChannelIdsEndDeprecated] + + val queryChannelRangeDeprecatedCodec: Codec[QueryChannelRangeDeprecated] = ( + ("chainHash" | binarydata(32)) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) + ).as[QueryChannelRangeDeprecated] + + val replyChannelRangeDeprecatedCodec: Codec[ReplyChannelRangeDeprecated] = ( + ("chainHash" | binarydata(32)) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("complete" | byte) :: + ("data" | varsizebinarydata) + ).as[ReplyChannelRangeDeprecated] + + val queryShortChannelIdsWithFlagsCodec: Codec[QueryShortChannelIdsWithFlags] = ( + ("chainHash" | binarydata(32)) :: + ("data" | varsizebinarydata) + ).as[QueryShortChannelIdsWithFlags] + + val replyShortChannelIdsWithFlagsEndCodec: Codec[ReplyShortChannelIdsWithFlagsEnd] = ( ("chainHash" | binarydata(32)) :: ("complete" | byte) - ).as[ReplyShortChannelIdsEndWithTimestamps] + ).as[ReplyShortChannelIdsWithFlagsEnd] val queryChannelRangeWithTimestampsCodec: Codec[QueryChannelRangeWithTimestamps] = ( ("chainHash" | binarydata(32)) :: @@ -370,11 +387,32 @@ object LightningMessageCodecs { ("data" | varsizebinarydata) ).as[ReplyChannelRangeWithTimestamps] - val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( + val queryChannelRangeWithChecksumsCodec: Codec[QueryChannelRangeWithChecksums] = ( ("chainHash" | binarydata(32)) :: - ("firstTimestamp" | uint32) :: - ("timestampRange" | uint32) - ).as[GossipTimestampFilter] + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) + ).as[QueryChannelRangeWithChecksums] + + val channelDigestInfoCodec: Codec[ChannelDigestInfo] = ( + ("shortChannelId" | shortchannelid) :: + ("timestamp1" | uint32) :: + ("timestamp2" | uint32) :: + ("checksum1" | uint32) :: + ("checksum2" | uint32) + ).as[ChannelDigestInfo] + + val channelRangeWithChecksumsDataCodec: Codec[ChannelRangeWithChecksumsData] = + discriminated[ChannelRangeWithChecksumsData].by(byte) + .\(EncodingTypes.UNCOMPRESSED) { case a@ChannelRangeWithChecksumsData(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(channelDigestInfoCodec)).as[ChannelRangeWithChecksumsData]) + .\(EncodingTypes.COMPRESSED_ZLIB) { case a@ChannelRangeWithChecksumsData(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(channelDigestInfoCodec))).as[ChannelRangeWithChecksumsData]) + + val replyChannelRangeWithChecksumsCodec: Codec[ReplyChannelRangeWithChecksums] = ( + ("chainHash" | binarydata(32)) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("complete" | byte) :: + ("data" | variableSizeBytes(uint16, channelRangeWithChecksumsDataCodec)) + ).as[ReplyChannelRangeWithChecksums] val lightningMessageCodec = discriminated[LightningMessage].by(uint16) .typecase(16, initCodec) @@ -405,15 +443,16 @@ object LightningMessageCodecs { .typecase(263, queryChannelRangeCodec) .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) - .typecase(1001, queryShortChannelIdsProtoCodec) - .typecase(1002, replyShortChanelIdsEndProtoCodec) - .typecase(1003, queryChannelRangeProtoCodec) - .typecase(1004, replyChannelRangeProtoCodec) - .typecase(1011, queryShortChannelIdsWithTimestampsCodec) - .typecase(1012, replyShortChanelIdsEndWithTimestampsCodec) + .typecase(1001, queryShortChannelIdsDeprecatedCodec) + .typecase(1002, replyShortChanelIdsEndDeprecatedCodec) + .typecase(1003, queryChannelRangeDeprecatedCodec) + .typecase(1004, replyChannelRangeDeprecatedCodec) + .typecase(1011, queryShortChannelIdsWithFlagsCodec) + .typecase(1012, replyShortChannelIdsWithFlagsEndCodec) .typecase(1013, queryChannelRangeWithTimestampsCodec) .typecase(1014, replyChannelRangeWithTimestampsCodec) - + .typecase(1017, queryChannelRangeWithChecksumsCodec) + .typecase(1018, replyChannelRangeWithChecksumsCodec) /** * A codec that caches serialized routing messages 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 2eb49ce3d4..c90c2826c1 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 @@ -212,11 +212,14 @@ case class PerHopPayload(shortChannelId: ShortChannelId, /** * * @param chainHash chain hash - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded + * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded */ case class QueryShortChannelIds(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash +case class ReplyShortChannelIdsEnd(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash + case class QueryChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash @@ -227,9 +230,6 @@ case class ReplyChannelRange(chainHash: BinaryData, complete: Byte, data: BinaryData) extends RoutingMessage with HasChainHash -case class ReplyShortChannelIdsEnd(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash - case class GossipTimestampFilter(chainHash: BinaryData, firstTimestamp: Long, timestampRange: Long) extends RoutingMessage with HasChainHash @@ -239,50 +239,74 @@ case class GossipTimestampFilter(chainHash: BinaryData, /** * * @param chainHash chain hash - * @param flag if flag == 1, don't send back channel announcements - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded + * @param flag if flag == 1, don't send back channel announcements + * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded */ -case class QueryShortChannelIdsProto(chainHash: BinaryData, - flag: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash +case class QueryShortChannelIdsDeprecated(chainHash: BinaryData, + flag: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash + +case class ReplyShortChannelIdsEndDeprecated(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash -case class QueryChannelRangeProto(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash +case class QueryChannelRangeDeprecated(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long) extends RoutingMessage with HasChainHash /** * - * @param chainHash chain hash - * @param firstBlockNum first block that is found in data + * @param chainHash chain hash + * @param firstBlockNum first block that is found in data * @param numberOfBlocks number of blocks spanned by data * @param complete - * @param data prefix + list of (short channel id + timestamp) values, where prefix specifies how the list is encoded + * @param data prefix + list of (short channel id + timestamp) values, where prefix specifies how the list is encoded */ -case class ReplyChannelRangeProto(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash - -case class ReplyShortChannelIdsEndProto(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash +case class ReplyChannelRangeDeprecated(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + data: BinaryData) extends RoutingMessage with HasChainHash // proposal for BOLT 1.1 channel queries -case class QueryShortChannelIdsWithTimestamps(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash - +case class QueryShortChannelIdsWithFlags(chainHash: BinaryData, + data: BinaryData) extends RoutingMessage with HasChainHash +case class ReplyShortChannelIdsWithFlagsEnd(chainHash: BinaryData, + complete: Byte) extends RoutingMessage with HasChainHash case class QueryChannelRangeWithTimestamps(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash - case class ReplyChannelRangeWithTimestamps(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, data: BinaryData) extends RoutingMessage with HasChainHash -case class ReplyShortChannelIdsEndWithTimestamps(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash +// 2nd proposal with checksums + +case class QueryChannelRangeWithChecksums(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long) extends RoutingMessage with HasChainHash + +case object EncodingTypes { + val UNCOMPRESSED: Byte = 0 + val COMPRESSED_ZLIB: Byte = 1 +} + +case class ChannelDigestInfo(shortChannelId: ShortChannelId, + timestamp1: Long, + timestamp2: Long, + checksum1: Long, + checksum2: Long) + +case class ChannelRangeWithChecksumsData(encoding: Byte, + array: List[ChannelDigestInfo]) + +case class ReplyChannelRangeWithChecksums(chainHash: BinaryData, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + data: ChannelRangeWithChecksumsData) extends RoutingMessage with HasChainHash \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 89c9f59dfa..a9aa319ce6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Block import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.{ReplyChannelRange, ReplyChannelRangeProto} +import fr.acinq.eclair.wire.{ReplyChannelRange, ReplyChannelRangeDeprecated} import org.scalatest.FunSuite import scala.collection.immutable.SortedMap @@ -60,7 +60,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex` messages (uncompressed format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -69,7 +69,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex` messages (zlib format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -78,7 +78,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex2` messages (uncompressed format)") { val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -87,7 +87,7 @@ class ChannelRangeQueriesSpec extends FunSuite { test("create `reply_channel_range_ex2` messages (zlib format)") { val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala index 8232629fd2..625737e153 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Block import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.ReplyChannelRangeProto +import fr.acinq.eclair.wire.ReplyChannelRangeDeprecated import org.scalatest.FunSuite import scala.collection.immutable.SortedMap @@ -15,7 +15,7 @@ class ChannelRangeQueriesWithTimestampsSpec extends FunSuite { test("create `reply_channel_range_ex` messages (uncompressed format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) @@ -24,7 +24,7 @@ class ChannelRangeQueriesWithTimestampsSpec extends FunSuite { test("create `reply_channel_range_ex` messages (zlib format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeProto(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) + val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala index 1e5aabacb1..8cb1421a74 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala @@ -23,8 +23,8 @@ class RoutingSyncProtoSpec extends TestKit(ActorSystem("test")) with FunSuiteLik val remoteNodeId = TestConstants.Bob.nodeParams.nodeId // ask router to send a channel range query - sender.send(router, SendChannelQueryProto(remoteNodeId, sender.ref)) - val QueryChannelRangeProto(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeProto] + sender.send(router, SendChannelQueryDeprecated(remoteNodeId, sender.ref)) + val QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeDeprecated] sender.expectMsgType[GossipTimestampFilter] @@ -42,14 +42,14 @@ class RoutingSyncProtoSpec extends TestKit(ActorSystem("test")) with FunSuiteLik val List(block3) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) // send first block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeProto(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeDeprecated(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) // router should ask for our first block of ids - val QueryShortChannelIdsProto(_, _, data1) = sender.expectMsgType[QueryShortChannelIdsProto] + val QueryShortChannelIdsDeprecated(_, _, data1) = sender.expectMsgType[QueryShortChannelIdsDeprecated] val (_, shortChannelIds1, false) = ShortChannelIdsBlock.decode(data1) assert(shortChannelIds1 == shortChannelIds.take(100)) // send second block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeProto(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeDeprecated(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) // router should not ask for more ids, it already has a pending query ! sender.expectNoMsg(1 second) @@ -73,10 +73,10 @@ class RoutingSyncProtoSpec extends TestKit(ActorSystem("test")) with FunSuiteLik sender.expectNoMsg(1 second) // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndProto(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndDeprecated(chainHash, 1.toByte))) // router should ask for our second block of ids - val QueryShortChannelIdsProto(_, _, data2) = sender.expectMsgType[QueryShortChannelIdsProto] + val QueryShortChannelIdsDeprecated(_, _, data2) = sender.expectMsgType[QueryShortChannelIdsDeprecated] val (_, shortChannelIds2, false) = ShortChannelIdsBlock.decode(data2) assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala index ea68a290a8..b75fc540df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala @@ -60,7 +60,7 @@ class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with Fu sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps))) // router should ask for our first block of ids - val QueryShortChannelIdsWithTimestamps(_, data1) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] + val QueryShortChannelIdsWithFlags(_, data1) = sender.expectMsgType[QueryShortChannelIdsWithFlags] val (_, shortChannelIdAndFlags1) = ShortChannelIdAndFlagsBlock.decode(data1) assert(shortChannelIdAndFlags1.keySet == shortChannelIds.take(Router.SHORTID_WINDOW)) assert(shortChannelIdAndFlags1.values.toSet == Set(ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) @@ -84,10 +84,10 @@ class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with Fu sender.expectNoMsg(1 second) // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) // router should ask for our second block of ids - val QueryShortChannelIdsWithTimestamps(_, data2) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] + val QueryShortChannelIdsWithFlags(_, data2) = sender.expectMsgType[QueryShortChannelIdsWithFlags] val (_, shortChannelIdAndFlags2) = ShortChannelIdAndFlagsBlock.decode(data2) assert(shortChannelIdAndFlags2.keySet == shortChannelIds.drop(Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) @@ -98,10 +98,10 @@ class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with Fu sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) // router should ask for our third block of ids - val QueryShortChannelIdsWithTimestamps(_, data3) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] + val QueryShortChannelIdsWithFlags(_, data3) = sender.expectMsgType[QueryShortChannelIdsWithFlags] val (_, shortChannelIdAndFlags3) = ShortChannelIdAndFlagsBlock.decode(data3) assert(shortChannelIdAndFlags3.keySet == shortChannelIds.drop(2 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) @@ -112,10 +112,10 @@ class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with Fu sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) // router should ask for our third block of ids - val QueryShortChannelIdsWithTimestamps(_, data4) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] + val QueryShortChannelIdsWithFlags(_, data4) = sender.expectMsgType[QueryShortChannelIdsWithFlags] val (_, shortChannelIdAndFlags4) = ShortChannelIdAndFlagsBlock.decode(data4) assert(shortChannelIdAndFlags4.keySet == shortChannelIds.drop(3 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) // send block #4 @@ -125,7 +125,7 @@ class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with Fu sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndWithTimestamps(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) awaitCond({ router.stateData.channels == initChannels @@ -152,7 +152,7 @@ class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with Fu sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block1.shortChannelIdAndTimestamps))) // router should ask for our new channel updates - val QueryShortChannelIdsWithTimestamps(_, data5) = sender.expectMsgType[QueryShortChannelIdsWithTimestamps] + val QueryShortChannelIdsWithFlags(_, data5) = sender.expectMsgType[QueryShortChannelIdsWithFlags] val (_, shortChannelIdAndFlags5) = ShortChannelIdAndFlagsBlock.decode(data5) assert(shortChannelIdAndFlags5.keySet == updatedIds) assert(shortChannelIdAndFlags5.values.toSet == Set(ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1)) 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 27999f1fae..feaebfc41c 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 @@ -22,12 +22,15 @@ import com.google.common.net.InetAddresses import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.router.ChannelRangeQueries.{UNCOMPRESSED_FORMAT, ZLIB_FORMAT} +import fr.acinq.eclair.router.{Announcements, ShortChannelIdAndTimestampsBlock} import fr.acinq.eclair.wire.LightningMessageCodecs._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomBytes, randomKey} import org.scalatest.FunSuite import scodec.bits.{BitVector, ByteVector, HexStringSyntax} +import scala.collection.{SortedMap, SortedSet} + /** * Created by PM on 31/05/2016. */ @@ -302,6 +305,70 @@ class LightningMessageCodecsSpec extends FunSuite { assert(bin === bin2) } + test("test TV codecs") { + + case class A(a: Byte, i: Int) + + import scodec.Codec + import scodec.codecs._ + + val codec: Codec[A] = + discriminated[A].by(byte) + .\(0) { case a@A(0, _) => a }((provide(0: Byte) :: uint8).as[A]) + .\(1) { case a@A(1, _) => a }((provide(1: Byte) :: uint16).as[A]) + + val bin1 = logToStdOut(codec).encode(A(0, 42)) + logToStdOut(codec).decode(bin1.require) + val bin2 = logToStdOut(codec).encode(A(1, 42)) + logToStdOut(codec).decode(bin2.require) + } + + test("basic channel range nonreg tests") { + + case class ChannelTimestampsInfo(shortChannelId: ShortChannelId, + node1Timestamp: Long, + node2Timestamp: Long) + + case class ChannelRangeWithTimestampsData(encoding: Byte, + array: List[ChannelTimestampsInfo]) + + import scodec.Codec + import scodec.codecs._ + + val channelTimestampsInfoCodec: Codec[ChannelTimestampsInfo] = ( + ("shortChannelId" | shortchannelid) :: + ("node1Timestamp" | uint32) :: + ("node2Timestamp" | uint32) + ).as[ChannelTimestampsInfo] + + val channelRangeWithTimestampsDataCodec: Codec[ChannelRangeWithTimestampsData] = + discriminated[ChannelRangeWithTimestampsData].by(byte) + .\(EncodingTypes.UNCOMPRESSED) { case a@ChannelRangeWithTimestampsData(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(channelTimestampsInfoCodec)).as[ChannelRangeWithTimestampsData]) + .\(EncodingTypes.COMPRESSED_ZLIB) { case a@ChannelRangeWithTimestampsData(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(channelTimestampsInfoCodec))).as[ChannelRangeWithTimestampsData]) + + val channels = SortedMap( + ShortChannelId(0xaa) -> (0x1000L, 0x1001L), + ShortChannelId(0xbb) -> (0x2000L, 0x2001L), + ShortChannelId(0xcc) -> (0x3000L, 0x3001L) + ) + + for (encoding <- List(UNCOMPRESSED_FORMAT, ZLIB_FORMAT)) { + + val encodedManual = ShortChannelIdAndTimestampsBlock + .encode(0, 42, channels.keySet, id => channels(id), encoding) + .head + .shortChannelIdAndTimestamps + + val decodedScodec = channelRangeWithTimestampsDataCodec.decode(BitVector.apply(encodedManual.data)).require.value + assert(decodedScodec.encoding == encoding) + assert(decodedScodec.array == channels.toList.map(x => ChannelTimestampsInfo(x._1, x._2._1, x._2._2))) + + val encodedScodec = channelRangeWithTimestampsDataCodec.encode(decodedScodec).require + assert(BinaryData(encodedScodec.toByteArray) == encodedManual) + } + + } + } object LightningMessageCodecsSpec { From 928d1654b3a0d2fa3d0ca30a614ba424870c5207 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 21 Jan 2019 17:40:29 +0100 Subject: [PATCH 19/86] major refactoring --- eclair-core/src/main/resources/reference.conf | 3 +- .../main/scala/fr/acinq/eclair/Features.scala | 7 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 25 +- .../eclair/router/ChannelRangeQueries.scala | 31 -- .../scala/fr/acinq/eclair/router/Router.scala | 513 ++++++------------ .../ShortChannelIdAndTimestampBlock.scala | 106 ---- .../ShortChannelIdAndTimestampsBlock.scala | 226 -------- .../eclair/router/ShortChannelIdsBlock.scala | 99 ---- .../eclair/wire/LightningMessageCodecs.scala | 68 ++- .../eclair/wire/LightningMessageTypes.scala | 92 ++-- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 6 +- .../router/ChannelRangeQueriesSpec.scala | 4 +- ...hannelRangeQueriesWithTimestampsSpec.scala | 4 +- .../fr/acinq/eclair/router/RouterSpec.scala | 2 +- .../router/RoutingSyncDeprecatedSpec.scala | 82 +++ .../eclair/router/RoutingSyncProtoSpec.scala | 83 --- .../acinq/eclair/router/RoutingSyncSpec.scala | 36 +- .../router/RoutingSyncWithChecksumsSpec.scala | 157 ++++++ .../RoutingSyncWithTimestampsSpec.scala | 161 ------ .../wire/LightningMessageCodecsSpec.scala | 105 ++-- 20 files changed, 599 insertions(+), 1211 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8a2596a9c2..f4d2fe4eb7 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -43,7 +43,8 @@ eclair { node-color = "49daaa" global-features = "" - local-features = "808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_proto + option_channel_range_queries_ex + local-features = "02808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_deprecated + option_channel_range_queries_extended + #local-features = "808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_deprecated override-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 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 c3b5f5d45e..95ef40cffc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -39,11 +39,8 @@ object Features { val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_MANDATORY = 14 val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_OPTIONAL = 15 - val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 16 - val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 17 - - val CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_MANDATORY = 18 - val CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_OPTIONAL = 19 + val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY = 16 + val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL = 17 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) 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 b586c91d94..70e67bcac8 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 @@ -106,39 +106,32 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) val remoteHasChannelRangeQueriesDeprecatedOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_OPTIONAL) val remoteHasChannelRangeQueriesDeprecatedMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_MANDATORY) - val remoteHasChannelRangeQueriesExOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesExMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY) - val remoteHasChannelRangeQueriesChecksumOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_MANDATORY) - val remoteHasChannelRangeQueriesChecksumMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_CHECKSUM_BIT_OPTIONAL) + val remoteHasChannelRangeQueriesExtendedOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL) + val remoteHasChannelRangeQueriesExtendedMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY) - log.info(s"$remoteNodeId has features: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") + log.info(s"peer has globalFeatures=${remoteInit.globalFeatures} localFeatures=${remoteInit.localFeatures}: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") if (Features.areSupported(remoteInit.localFeatures)) { d.origin_opt.foreach(origin => origin ! "connected") if (remoteHasInitialRoutingSync) { - if (remoteHasChannelRangeQueriesChecksumOptional || remoteHasChannelRangeQueriesChecksumMandatory) { + if (remoteHasChannelRangeQueriesExtendedOptional || remoteHasChannelRangeQueriesExtendedMandatory) { // if they support extended channel queries we do nothing, they will send us their filters - log.info("{} has set initial routing sync and support checksum channel range queries, we do nothing (they will send us a query)", remoteNodeId) - } else if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { - // if they support extended channel queries we do nothing, they will send us their filters - log.info("{} has set initial routing sync and support extended channel range queries, we do nothing (they will send us a query)", remoteNodeId) + log.info("peer has set initial routing sync and supports extended channel range queries, we do nothing (they will send us a query)") } else if (remoteHasChannelRangeQueriesDeprecatedOptional || remoteHasChannelRangeQueriesDeprecatedMandatory) { // if they support extended channel queries we do nothing, they will send us their filters - log.info("{} has set initial routing sync and support proto channel range queries, we do nothing (they will send us a query)", remoteNodeId) + log.info("peer has set initial routing sync and supports deprecated channel range queries, we do nothing (they will send us a query)") } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries we do nothing, they will send us their filters - log.info("{} has set initial routing sync and support channel range queries, we do nothing (they will send us a query)", remoteNodeId) + log.info("peer has set initial routing sync and supports channel range queries, we do nothing (they will send us a query)") } else { // "old" nodes, do as before router ! GetRoutingState } } - if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { + + if (remoteHasChannelRangeQueriesExtendedOptional || remoteHasChannelRangeQueriesExtendedMandatory) { // if they support extended channel queries, always ask for their filter router ! SendChannelQueryWithChecksums(remoteNodeId, d.transport) - } else if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { - // if they support extended channel queries, always ask for their filter - router ! SendChannelQueryWithTimestamps(remoteNodeId, d.transport) } else if (remoteHasChannelRangeQueriesDeprecatedOptional || remoteHasChannelRangeQueriesDeprecatedMandatory) { // if they support proto channel queries, always ask for their filter router ! SendChannelQueryDeprecated(remoteNodeId, d.transport) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala deleted file mode 100644 index b9c8a63a53..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala +++ /dev/null @@ -1,31 +0,0 @@ -package fr.acinq.eclair.router - -import java.io.InputStream - -import scala.annotation.tailrec - -object ChannelRangeQueries { - - val UNCOMPRESSED_FORMAT = 0.toByte - val ZLIB_FORMAT = 1.toByte - val INCLUDE_CHANNEL_UPDATE_1 = 1.toByte - val INCLUDE_CHANNEL_UPDATE_2 = 2.toByte - val INCLUDE_ANNOUNCEMENT = 4.toByte - - def includeAnnoucement(flag: Byte) = (flag & INCLUDE_ANNOUNCEMENT) != 0 - - def includeUpdate1(flag: Byte) = (flag & INCLUDE_CHANNEL_UPDATE_1) != 0 - - def includeUpdate2(flag: Byte) = (flag & INCLUDE_CHANNEL_UPDATE_2) != 0 - - // read bytes from an input stream - // zipped input stream often returns less bytes than what you want to read - @tailrec - def readBytes(buffer: Array[Byte], input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, buffer.length - offset) match { - case len if len <= 0 => len - case len if len == buffer.length => buffer.length - case len if offset + len == buffer.length => buffer.length - case len => readBytes(buffer, input, offset + len) - } - -} 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 01c2ac21d0..03029625f1 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 @@ -60,10 +60,7 @@ case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) // remove ASAP i.e as soon as mobile apps have been updated with the new queries below case class SendChannelQueryDeprecated(remoteNodeId: PublicKey, to: ActorRef) -// channel queries with 2 extra timestamps (one per chanel update), proposed for BOLT 1.1 -case class SendChannelQueryWithTimestamps(remoteNodeId: PublicKey, to: ActorRef) - -// channel queries with 2 extra timestamps (one per chanel update) and a checksum +// channel queries with 2 extra timestamps (one per chanel update) and a checksum, proposed for BOLT 1.1 case class SendChannelQueryWithChecksums(remoteNodeId: PublicKey, to: ActorRef) case object GetRoutingState @@ -71,13 +68,14 @@ case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterab case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) -case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int, - outdated: SortedSet[ShortChannelId] = SortedSet.empty[ShortChannelId], totalOutdatedCount: Int = 0, - outdatedEx: SortedSet[ShortChannelIdAndFlag] = SortedSet.empty[ShortChannelIdAndFlag], totalOutdatedExCount: Int = 0) { - // returns a sync progress indicator (1 means fully synced) +case class Sync(pending: List[RoutingMessage], total: Int) { + + /** + * NB: progress is in terms of requests, not individual channels + * @return returns a sync progress indicator (1 means fully synced) + */ def progress: Double = { - val inflight = (missing.size + outdated.size + outdatedEx.size) * 1.0 - val total = totalMissingCount + totalOutdatedCount + totalOutdatedExCount + val inflight = pending.size * 1.0 if (total == 0) 1.0 else inflight / total } } @@ -96,105 +94,6 @@ case class Data(nodes: Map[PublicKey, NodeAnnouncement], // for which we have not yet received an 'end' message ) -object Data { - /** - * Process a reply_channel_range message - * - * @param state current state - * @param remoteNodeId id of the node that sent the reply - * @param replyChannelRangeEx reply_channel_range message - * @return a (new state, query) pair, where query is an optional query_short_channel_ids message to be sent to be - * sent back to the remote node - */ - def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx: ReplyChannelRangeWithTimestamps): (Data, Option[QueryShortChannelIdsWithFlags]) = { - val ReplyChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks, _, data) = replyChannelRangeEx - val (format, theirTimestampMap) = ShortChannelIdAndTimestampsBlock.decode(data) - val theirShortChannelIds = theirTimestampMap.keySet - - // keep our ids that match [block, block + numberOfBlocks] - val ourShortChannelIds: SortedSet[ShortChannelId] = state.channels.keySet.filter(id => { - val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) - height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) - }) // TODO: this doesn't work if there is overlap in the blocks - - // missing are the ones we don't have - val missing = (theirShortChannelIds -- ourShortChannelIds).map(id => ShortChannelIdAndFlag(id, ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) - - def computeFlag(id: ShortChannelId): Byte = { - val (ourts1, ourts2) = Router.getTimestamps(state.channels, state.updates)(id) - val (theirts1, theirts2) = theirTimestampMap.getOrElse(id, (0L, 0L)) - var flag = 0 - if (ourts1 < theirts1) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 - if (ourts2 < theirts2) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 - flag.toByte - } - - // outdated are the ones for which our update timestamp is older that theirs - val outdated = ourShortChannelIds - .filter(id => theirShortChannelIds.contains(id)) - .map(id => ShortChannelIdAndFlag(id, computeFlag(id))) - .filterNot(_.flag == 0.toByte) - - // combine missing and outdated sets - val shortChannelIdAndFlags = missing ++ outdated - - val (newState, query) = if (shortChannelIdAndFlags.nonEmpty) { - state.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = shortChannelIdAndFlags.splitAt(Router.SHORTID_WINDOW) - val query = QueryShortChannelIdsWithFlags(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, format)) - val state1 = state.copy(sync = state.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) - (state1, Some(query)) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - val state1 = state.copy(sync = state.sync + (remoteNodeId -> sync.copy(outdatedEx = sync.outdatedEx ++ shortChannelIdAndFlags, totalOutdatedExCount = sync.totalOutdatedExCount + shortChannelIdAndFlags.size))) - (state1, None) - } - } else (state, None) // nothing to do - - (newState, query) - } - - def handleReplyChannelRange(state: Data, remoteNodeId: PublicKey, replyChannelRangeEx: ReplyChannelRangeWithChecksums): (Data, Option[QueryShortChannelIdsWithFlags]) = { - val shortChannelIdAndFlagsList = replyChannelRangeEx - .data - .array - .map { channelInfo => - var flag = 0 - if (state.channels.contains(channelInfo.shortChannelId)) { - val ourInfo = Router.getChannelDigestInfo(state.channels, state.updates)(channelInfo.shortChannelId) - if (ourInfo.timestamp1 < channelInfo.timestamp1 && ourInfo.checksum1 != channelInfo.checksum1) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 - if (ourInfo.timestamp2 < channelInfo.timestamp2 && ourInfo.checksum2 != channelInfo.checksum2) flag = flag | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 - } else { - // we don't know this channel: we request everything - flag = flag | ChannelRangeQueries.INCLUDE_ANNOUNCEMENT | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1 | ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_2 - } - ShortChannelIdAndFlag(channelInfo.shortChannelId, flag.toByte) - } - .filter(_.flag != 0) - - val shortChannelIdAndFlags = collection.SortedSet(shortChannelIdAndFlagsList: _*) - - val (newState, query) = if (shortChannelIdAndFlags.nonEmpty) { - state.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = shortChannelIdAndFlags.splitAt(Router.SHORTID_WINDOW) - val query = QueryShortChannelIdsWithFlags(replyChannelRangeEx.chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, replyChannelRangeEx.data.encoding)) - val state1 = state.copy(sync = state.sync + (remoteNodeId -> Sync(SortedSet(), 0, SortedSet(), 0, rest, shortChannelIdAndFlags.size))) - (state1, Some(query)) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - val state1 = state.copy(sync = state.sync + (remoteNodeId -> sync.copy(outdatedEx = sync.outdatedEx ++ shortChannelIdAndFlags, totalOutdatedExCount = sync.totalOutdatedExCount + shortChannelIdAndFlags.size))) - (state1, None) - } - } else (state, None) // nothing to do - - (newState, query) - } -} - sealed trait State case object NORMAL extends State @@ -535,18 +434,6 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) - case Event(SendChannelQueryWithTimestamps(remoteNodeId, remote), d) => - // ask for everything - val query = QueryChannelRangeWithTimestamps(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) - log.info("sending query_channel_range_ex={}", query) - remote ! query - // we also set a pass-all filter for now (we can update it later) - val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) - remote ! filter - // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and - // will start a new complete sync process - stay using d.copy(sync = d.sync - remoteNodeId) - case Event(SendChannelQueryWithChecksums(remoteNodeId, remote), d) => // ask for everything val query = QueryChannelRangeWithChecksums(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) @@ -616,154 +503,108 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received query_channel_range={}", routingMessage) - // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] + log.info("received {}", routingMessage) + // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) - // TODO: we don't compress to be compatible with old mobile apps, switch to ZLIB ASAP - // Careful: when we remove GZIP support, eclair-wallet 0.3.0 will stop working i.e. channels to ACINQ nodes will not - // work anymore - val blocks = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) - log.info("sending back reply_channel_range with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - // there could be several reply_channel_range messages for a single query - val replies = blocks.map(block => ReplyChannelRange(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - replies.foreach(reply => transport ! reply) + log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + split(shortChannelIds) + .foreach(chunk => ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds))) stay case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received query_channel_range_proto={}", routingMessage) - // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) - val blocks = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamp(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - log.info("sending back reply_channel_range_proto with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - val replies = blocks.map(block => ReplyChannelRangeDeprecated(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - replies.foreach(reply => transport ! reply) - stay - - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - log.info("received query_channel_range_ex={}", routingMessage) - // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] + log.info("received {}", routingMessage) + // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) - val blocks = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamps(d.channels, d.updates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - log.info("sending back reply_channel_range_ex with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - val replies = blocks.map(block => ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - replies.foreach(reply => transport ! reply) + log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + split(shortChannelIds) + .foreach(chunk => transport ! ReplyChannelRangeDeprecated(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds.map(id => ShortChannelIdWithTimestamp(id, getTimestamp(d.channels, d.updates)(id)))))) stay case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received query_channel_range_with_checksums={}", routingMessage) - // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] + log.info("received {}", routingMessage) + // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) - val replies = shortChannelIds - .toList - .map(getChannelDigestInfo(d.channels, d.updates)) - .grouped(2000) // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - .map { group => - // TODO: check if the default value is correct - val firstBlock: Long = group.headOption.map(i => ShortChannelId.coordinates(i.shortChannelId).blockHeight.toLong).getOrElse(firstBlockNum) - val numBlocks: Long = group.lastOption.map(i => ShortChannelId.coordinates(i.shortChannelId).blockHeight.toLong).getOrElse(numberOfBlocks) - // complete = 1 because we maintain complete information for this chain - ReplyChannelRangeWithChecksums(chainHash, firstBlock, numBlocks, complete = 1, ChannelRangeWithChecksumsData(EncodingTypes.UNCOMPRESSED, group)) - } - log.info("sending back reply_channel_range_with_checksums with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - replies.foreach(reply => transport ! reply) + log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) + split(shortChannelIds) + .foreach(chunk => transport ! ReplyChannelRangeWithChecksums(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates))))) stay case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (format, theirShortChannelIds, useGzip) = ShortChannelIdsBlock.decode(data) + val theirShortChannelIds: SortedSet[ShortChannelId] = SortedSet(data.array: _*) val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) val missing: SortedSet[ShortChannelId] = theirShortChannelIds -- ourShortChannelIds - log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={} useGzip={}", missing.size, format, useGzip) - val d1 = if (missing.nonEmpty) { - // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not - // send another query_short_channel_ids query if they're still processing one - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIds(chainHash, router.ShortChannelIdsBlock.encodeSingle(slice, format, useGzip)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> Sync(sync.missing ++ missing, sync.totalMissingCount + missing.size))) - } - } else d - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 + log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={}", missing.size, data.encoding) + // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) + val replies = missing + .grouped(SHORTID_WINDOW) + .map(chunk => QueryShortChannelIds(chainHash, data = EncodedShortChannelIds(data.encoding, chunk.toList))) + .toList + val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) + // we only send a rely right away if there were no pending requests + replynow_opt.foreach(transport ! _) + context.system.eventStream.publish(syncProgress(sync1)) + stay using d.copy(sync = sync1) - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeDeprecated(chainHash, _, _, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - - val (format, theirTimestampMap) = ShortChannelIdAndTimestampBlock.decode(data) - val theirShortChannelIds = theirTimestampMap.keySet - // keep our ids that match [block, block + numberOfBlocks] - val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(id => { - val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) - height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) - }) - - // missing are the ones we don't have - val missing = theirShortChannelIds -- ourShortChannelIds - - // outdated are the ones for which our update timestamp is older that theirs - val outdated = ourShortChannelIds.filter(id => { - theirShortChannelIds.contains(id) && Router.getTimestamp(d.channels, d.updates)(id) < theirTimestampMap(id) - }) - log.info("received reply_channel_range_proto, we're missing {} channel announcements and we have {} outdated ones, format={} ", missing.size, outdated.size, format) - // we first sync missing channels, then outdated ones - val d1 = if (missing.nonEmpty) { - // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not - // send another query_short_channel_ids query if they're still processing one - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsDeprecated(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size, outdated, outdated.size))) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = sync.missing ++ missing, totalMissingCount = sync.totalMissingCount + missing.size))) - } - } else if (outdated.nonEmpty) { - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsDeprecated(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, format, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet(), 0, outdated, outdated.size))) - case Some(sync) => - // we already have a pending query with this peer, add outdated ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = sync.outdated ++ outdated, totalOutdatedCount = sync.totalOutdatedCount + outdated.size))) + val missing = data.array + .filter { channelInfo => + // we request unknown channels + !d.channels.contains(channelInfo.shortChannelId) || + // and known channels for which our timestamp is older than theirs + Router.getTimestamp(d.channels, d.updates)(channelInfo.shortChannelId) < channelInfo.timestamp } - } else d - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 - - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - val (d1, query) = Data.handleReplyChannelRange(d, remoteNodeId, routingMessage) - query.map(q => transport ! q) - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 + .map(_.shortChannelId) + log.info("received reply_channel_range_deprecated, we're missing {} channel announcements/updates, format={}", missing.size, data.encoding) + // TODO: simplification! we always request all data (this is deprecated anyway) + val flag = (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte + // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) + val replies = missing + .grouped(SHORTID_WINDOW) + .map(chunk => QueryShortChannelIdsDeprecated(chainHash, flag, data = EncodedShortChannelIds(data.encoding, chunk))) + .toList + val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) + // we only send a rely right away if there were no pending requests + replynow_opt.foreach(transport ! _) + context.system.eventStream.publish(syncProgress(sync1)) + stay using d.copy(sync = sync1) - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeWithChecksums(chainHash, _, _, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (d1, query) = Data.handleReplyChannelRange(d, remoteNodeId, routingMessage) - d1.sync.get(remoteNodeId).map {sync => - log.info(s"sync.totalOutdatedExCount=${sync.totalOutdatedExCount}") - } - query.map(q => transport ! q) - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 + val shortChannelIdAndFlags = data.array + .map { channelInfo => + var flag = 0 + if (d.channels.contains(channelInfo.shortChannelId)) { + val ourInfo = Router.getChannelDigestInfo(d.channels, d.updates)(channelInfo.shortChannelId) + if (ourInfo.timestamp1 < channelInfo.timestamp1 && ourInfo.checksum1 != channelInfo.checksum1) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 + if (ourInfo.timestamp2 < channelInfo.timestamp2 && ourInfo.checksum2 != channelInfo.checksum2) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_2 + } else { + // we don't know this channel: we request everything + flag = flag | FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2 + } + ShortChannelIdAndFlag(channelInfo.shortChannelId, flag.toByte) + } + .filter(_.flag != 0) + log.info("received reply_channel_range_with_checksums, we're missing {} channel announcements/updates, format={}", shortChannelIdAndFlags.size, data.encoding) + // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) + val replies = shortChannelIdAndFlags + .grouped(SHORTID_WINDOW) + .map(chunk => QueryShortChannelIdsWithFlags(chainHash, data = EncodedShortChannelIdsAndFlag(data.encoding, chunk))) + .toList + val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) + // we only send a rely right away if there were no pending requests + replynow_opt.foreach(transport ! _) + context.system.eventStream.publish(syncProgress(sync1)) + stay using d.copy(sync = sync1) // standard query message: a list of channel ids case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (_, shortChannelIds, useGzip) = ShortChannelIdsBlock.decode(data) - log.info("received query_short_channel_ids for {} channel announcements, useGzip={}", shortChannelIds.size, useGzip) + val shortChannelIds = data.array + log.info("received query_short_channel_ids for {} channel announcements", shortChannelIds.size) shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) @@ -779,15 +620,15 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // extended query message: a flag and a list of channel ids case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsDeprecated(chainHash, flag, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (_, shortChannelIds, _) = ShortChannelIdsBlock.decode(data) + val shortChannelIds = data.array log.info("received query_short_channel_ids_proto for {} channel announcements, flag={}", shortChannelIds.size, flag) shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => - if (flag == 1.toByte) transport ! ca - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) + if (FlagTypes.includeAnnouncement(flag)) transport ! ca + if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) + if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } }) transport ! ReplyShortChannelIdsEndDeprecated(chainHash, 1) @@ -797,88 +638,33 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // new extended query message: a list of [channel id + flag] case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsWithFlags(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (_, shortChannelIdAndFlags) = ShortChannelIdAndFlagsBlock.decode(data) + val shortChannelIdAndFlags = data.array log.info("received query_short_channel_ids_ex for {} channel announcements", shortChannelIdAndFlags.size) shortChannelIdAndFlags.foreach { - case (shortChannelId, flag) => { + case ShortChannelIdAndFlag(shortChannelId, flag) => { d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => - if (ChannelRangeQueries.includeAnnoucement(flag)) transport ! ca - // specs says that in channel announcements we have node_id_1 transport ! u) - if (ChannelRangeQueries.includeAnnoucement(flag) || ChannelRangeQueries.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) + if (FlagTypes.includeAnnouncement(flag)) transport ! ca + if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) + if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } } } transport ! ReplyShortChannelIdsWithFlagsEnd(chainHash, 1) stay - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEnd(chainHash, complete)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsEnd), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received reply_short_channel_ids_end={}", routingMessage) - // have we more channels to ask this peer? - val d1 = d.sync.get(remoteNodeId) match { - case Some(sync) if sync.missing.nonEmpty => - log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) - val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIds(chainHash, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) - case Some(sync) if sync.missing.isEmpty => - // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove - // the remote peer from our map - d.copy(sync = d.sync - remoteNodeId) - case _ => - d - } - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 + stay using handleSyncEnd(d, remoteNodeId, transport) - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndDeprecated(chainHash, complete)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsEndDeprecated), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received reply_short_channel_ids_end_proto={}", routingMessage) - // have we more channels to ask this peer? - val d1 = d.sync.get(remoteNodeId) match { - case Some(sync) if sync.missing.nonEmpty => - log.info(s"asking {} for the next slice of missing short_channel_ids", remoteNodeId) - val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsDeprecated(chainHash, 1.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) - case Some(sync) if sync.outdated.nonEmpty => - log.info(s"asking {} for the next slice of outdated short_channel_ids", remoteNodeId) - val (slice, rest) = sync.outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsDeprecated(chainHash, 0.toByte, ShortChannelIdsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = rest))) - case Some(sync) if sync.missing.isEmpty && sync.outdated.isEmpty => - // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove - // the remote peer from our map - d.copy(sync = d.sync - remoteNodeId) - case _ => - d - } - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 + stay using handleSyncEnd(d, remoteNodeId, transport) - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsWithFlagsEnd(chainHash, complete)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsWithFlagsEnd), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received reply_short_channel_ids_end_ex={}", routingMessage) - // have we more channels to ask this peer? - val d1 = d.sync.get(remoteNodeId) match { - case Some(sync) if sync.outdatedEx.nonEmpty => - log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) - val (slice, rest) = sync.outdatedEx.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsWithFlags(chainHash, ShortChannelIdAndFlagsBlock.encodeSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdatedEx = rest))) - case Some(sync) if sync.outdatedEx.isEmpty => - // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove - // the remote peer from our map - d.copy(sync = d.sync - remoteNodeId) - case _ => - d - } - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 + stay using handleSyncEnd(d, remoteNodeId, transport) } @@ -1009,18 +795,20 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // when we're sending updates to ourselves (transport_opt, remoteNodeId_opt) match { case (Some(transport), Some(remoteNodeId)) => + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(u.shortChannelId))) d.sync.get(remoteNodeId) match { case Some(sync) => // we already have a pending request to that node, let's add this channel to the list and we'll get it later - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = sync.missing + u.shortChannelId, totalMissingCount = sync.totalMissingCount + 1))) + // TODO: we only request channels with old style channel_query + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(pending = sync.pending :+ query, total = sync.total + 1))) case None => // we send the query right away - transport ! QueryShortChannelIds(u.chainHash, ShortChannelIdsBlock.encodeSingle(Seq(u.shortChannelId), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(missing = SortedSet(u.shortChannelId), totalMissingCount = 1))) + transport ! query + d.copy(sync = d.sync + (remoteNodeId -> Sync(pending = Nil, total = 1))) } case _ => // we don't know which node this update came from (maybe it was stashed and the channel got pruned in the meantime or some other corner case). - // or we don't have a transport to send our query with. + // or we don't have a transport to send our query to. // anyway, that's not really a big deal because we have removed the channel from the pruned db so next time it shows up we will revalidate it d } @@ -1029,6 +817,27 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom d } + def handleSyncEnd(d: Data, remoteNodeId: PublicKey, transport: ActorRef): Data = { + // have we more channels to ask this peer? + val sync1 = d.sync.get(remoteNodeId) match { + case Some(sync) => + sync.pending match { + case nextRequest +: rest => + log.info(s"asking for the next slice of short_channel_ids (remaining=${sync.pending.size}/${sync.total})") + transport ! nextRequest + d.sync + (remoteNodeId -> sync.copy(pending = rest)) + case Nil => + // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove + // the remote peer from our map + log.info(s"sync complete (total=${sync.total})") + d.sync - remoteNodeId + } + case _ => d.sync + } + context.system.eventStream.publish(syncProgress(sync1)) + d.copy(sync = sync1) + } + override def mdc(currentMessage: Any): MDC = currentMessage match { case SendChannelQuery(remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) case PeerRoutingMessage(_, remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) @@ -1106,11 +915,11 @@ object Router { height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } - def syncProgress(d: Data): SyncProgress = - if (d.sync.isEmpty) { + def syncProgress(sync: Map[PublicKey, Sync]): SyncProgress = + if (sync.isEmpty) { SyncProgress(1) } else { - SyncProgress(d.sync.values.map(_.progress).sum / d.sync.values.size) + SyncProgress(sync.values.map(_.progress).sum / sync.values.size) } /** @@ -1146,32 +955,15 @@ object Router { timestamp } - def getTimestamps(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(id: ShortChannelId): (Long, Long) = { - val ca = channels(id) - val opt1 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)) - val opt2 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)) - val timestamps = (opt1, opt2) match { - // TODO: ??? this needs to be checked - case (Some(u1), Some(u2)) if Announcements.isNode1(u1.channelFlags) && !Announcements.isNode1(u2.channelFlags) => (u1.timestamp, u2.timestamp) - case (Some(u1), Some(u2)) if !Announcements.isNode1(u1.channelFlags) && Announcements.isNode1(u2.channelFlags) => (u2.timestamp, u1.timestamp) - case (Some(u1), None) if Announcements.isNode1(u1.channelFlags) => (u1.timestamp, 0L) - case (Some(u1), None) => (0L, u1.timestamp) - case (None, Some(u2)) if Announcements.isNode1(u2.channelFlags) => (u2.timestamp, 0L) - case (None, Some(u2)) => (0L, u2.timestamp) - case (None, None) => (0L, 0L) - } - timestamps - } - - def getChannelDigestInfo(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId): ChannelDigestInfo = { + def getChannelDigestInfo(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId): ShortChannelIdWithChecksums = { val c = channels(shortChannelId) val u1_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) - val u2_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) + val u2_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) val timestamp1 = u1_opt.map(_.timestamp).getOrElse(0L) val timestamp2 = u2_opt.map(_.timestamp).getOrElse(0L) val checksum1 = u1_opt.map(getChecksum).getOrElse(0L) val checksum2 = u2_opt.map(getChecksum).getOrElse(0L) - ChannelDigestInfo(shortChannelId, timestamp1, timestamp2, checksum1, checksum2) + ShortChannelIdWithChecksums(shortChannelId, timestamp1, timestamp2, checksum1, checksum2) } def getChecksum(u: ChannelUpdate): Long = { @@ -1182,6 +974,47 @@ object Router { checksum.getValue } + case class ShortChannelIdsChunk(firstBlock: Long, numBlocks: Long, shortChannelIds: List[ShortChannelId]) + + /** + * Have to split ids because otherwise message could be too big + * there could be several reply_channel_range messages for a single query + * + * @param shortChannelIds + * @return + */ + def split(shortChannelIds: SortedSet[ShortChannelId]): List[ShortChannelIdsChunk] = { + // TODO: this is wrong because it can split blocks + shortChannelIds + .grouped(2000) // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + .toList + .map { group => + // NB: group is never empty + val firstBlock: Long = ShortChannelId.coordinates(group.head).blockHeight.toLong + val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong + ShortChannelIdsChunk(firstBlock, numBlocks, group.toList) + } + } + + def updateSync(syncMap: Map[PublicKey, Sync], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Sync], Option[RoutingMessage]) = { + pending match { + case head +: rest => + // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not + // send another query_short_channel_ids query if they're still processing one + syncMap.get(remoteNodeId) match { + case None => + // we don't have a pending query with this peer, let's send it + (syncMap + (remoteNodeId -> Sync(rest, pending.size)), Some(head)) + case Some(sync) => + // we already have a pending query with this peer, add missing ids to our "sync" state + (syncMap + (remoteNodeId -> Sync(sync.pending ++ pending, sync.total + pending.size)), None) + } + case Nil => + // there is nothing to send + (syncMap, None) + } + } + /** * https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala deleted file mode 100644 index 14fafd0d57..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampBlock.scala +++ /dev/null @@ -1,106 +0,0 @@ -package fr.acinq.eclair.router - -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} -import java.nio.ByteOrder -import java.util.zip.{DeflaterOutputStream, InflaterInputStream} - -import fr.acinq.bitcoin.{BinaryData, Protocol} -import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.router.ChannelRangeQueries.{UNCOMPRESSED_FORMAT, ZLIB_FORMAT, readBytes} - -import scala.annotation.tailrec -import scala.collection.SortedSet -import scala.collection.immutable.SortedMap - -/** - * - * README: this is an obsolete design for channel range queries, used by eclair only, and to be dropped ASAP - * @param firstBlock fist block covered by this message - * @param numBlocks number of blocks covered by this message - * @param shortChannelIdAndTimestamps packed list of short channel id + the most recent channel update timestamp - */ -case class ShortChannelIdAndTimestampBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: BinaryData) - -object ShortChannelIdAndTimestampBlock { - /** - * Compressed a sequence of *sorted* short channel id. - * - * @param shortChannelIds must be sorted beforehand - * @return a sequence of encoded short channel ids - */ - def encode(firstBlockIn: Long, numBlocksIn: Long, - shortChannelIds: SortedSet[ShortChannelId], timestamp: ShortChannelId => Long, - format: Byte): List[ShortChannelIdAndTimestampBlock] = { - if (shortChannelIds.isEmpty) { - // special case: reply with an "empty" block - List(ShortChannelIdAndTimestampBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) - } else { - // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { - case UNCOMPRESSED_FORMAT => 4000 - case ZLIB_FORMAT => 8000 // TODO: do something less simplistic... - } - shortChannelIds.grouped(count).map(ids => { - val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { - val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight - val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 - (firstBlock, numBlocks) - } - val encoded = encodeSingle(ids, timestamp, format) - ShortChannelIdAndTimestampBlock(firstBlock, numBlocks, encoded) - }).toList - } - } - - def encodeSingle(shortChannelIds: Iterable[ShortChannelId], timestamp: ShortChannelId => Long, format: Byte): BinaryData = { - val bos = new ByteArrayOutputStream() - bos.write(format) - val out = format match { - case UNCOMPRESSED_FORMAT => bos - case ZLIB_FORMAT => new DeflaterOutputStream(bos) - } - shortChannelIds.foreach(id => { - Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN) - Protocol.writeUInt32(timestamp(id), out, ByteOrder.BIG_ENDIAN) - }) - out.close() - bos.toByteArray - } - - /** - * Decompress a zipped sequence of sorted [short channel id | timestamp] values. - * - * @param data - * @return a sorted map of short channel id -> timestamp - */ - def decode(data: BinaryData): (Byte, SortedMap[ShortChannelId, Long]) = { - val format = data.head - if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, Long]) else { - val buffer = new Array[Byte](12) - - // read until there's nothing left - @tailrec - def loop(input: InputStream, acc: SortedMap[ShortChannelId, Long]): SortedMap[ShortChannelId, Long] = { - val check = readBytes(buffer, input) - if (check <= 0) acc else loop(input, acc + (ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) -> Protocol.uint32(buffer.drop(8), ByteOrder.BIG_ENDIAN))) - } - - def readAll() = { - val bis = new ByteArrayInputStream(data.tail.toArray) - val input = format match { - case UNCOMPRESSED_FORMAT => bis - case ZLIB_FORMAT => new InflaterInputStream(bis) - } - try { - (format, loop(input, SortedMap.empty[ShortChannelId, Long])) - } - finally { - input.close() - } - } - - readAll() - } - } -} - diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala deleted file mode 100644 index 35f2c0975e..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdAndTimestampsBlock.scala +++ /dev/null @@ -1,226 +0,0 @@ -package fr.acinq.eclair.router - -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, OutputStream} -import java.nio.ByteOrder -import java.util.zip.{DeflaterOutputStream, InflaterInputStream} - -import fr.acinq.bitcoin.{BinaryData, Protocol} -import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.router.ChannelRangeQueries._ - -import scala.annotation.tailrec -import scala.collection.SortedSet -import scala.collection.immutable.SortedMap - -/** - * - * @param firstBlock fist block covered by this message - * @param numBlocks number of blocks covered by this message - * @param shortChannelIdAndTimestamps packed list of short channel id + timestamp #1 + timestamp #2 - */ -case class ShortChannelIdAndTimestampsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: BinaryData) - -object ShortChannelIdAndTimestampsBlock { - /** - * Compressed a sequence of *sorted* short channel id and timestamps - * - * @param shortChannelIds must be sorted beforehand - * @return a sequence of encoded short channel ids - */ - def encode(firstBlockIn: Long, numBlocksIn: Long, - shortChannelIds: SortedSet[ShortChannelId], timestamp: ShortChannelId => (Long, Long), - format: Byte): List[ShortChannelIdAndTimestampsBlock] = { - if (shortChannelIds.isEmpty) { - // special case: reply with an "empty" block - List(ShortChannelIdAndTimestampsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) - } else { - // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { - case UNCOMPRESSED_FORMAT => 2000 - case ZLIB_FORMAT => 5000 // TODO: do something less simplistic... - } - shortChannelIds.grouped(count).map(ids => { - val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { - val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight - val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 - (firstBlock, numBlocks) - } - val encoded = encodeSingle(ids, timestamp, format) - ShortChannelIdAndTimestampsBlock(firstBlock, numBlocks, encoded) - }).toList - } - } - - def writeChannelIdAndTimestamps(out: OutputStream, id: ShortChannelId, timestamp1: Long, timestamp2: Long): Unit = { - Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN) - Protocol.writeUInt32(timestamp1, out, ByteOrder.BIG_ENDIAN) - Protocol.writeUInt32(timestamp2, out, ByteOrder.BIG_ENDIAN) - } - - def writeChannelIdAndTimestamps(out: OutputStream, id: ShortChannelId, timestamp: ShortChannelId => (Long, Long)): Unit = { - val (ts1, ts2) = timestamp(id) - writeChannelIdAndTimestamps(out, id, ts1, ts2) - } - - def encodeSingle(shortChannelIds: Iterable[ShortChannelId], timestamp: ShortChannelId => (Long, Long), format: Byte): BinaryData = { - val bos = new ByteArrayOutputStream() - bos.write(format) - val out = format match { - case UNCOMPRESSED_FORMAT => bos - case ZLIB_FORMAT => new DeflaterOutputStream(bos) - } - shortChannelIds.foreach(id => writeChannelIdAndTimestamps(out, id, timestamp)) - out.close() - bos.toByteArray - } - - /** - * Decompress a zipped sequence of sorted [short channel id | timestamp1 | timestamp2 ] values. - * - * @param data - * @return a sorted map of short channel id -> timestamps - */ - def decode(data: BinaryData): (Byte, SortedMap[ShortChannelId, (Long, Long)]) = { - val format = data.head - if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, (Long, Long)]) else { - val buffer = new Array[Byte](16) - - // read until there's nothing left - @tailrec - def loop(input: InputStream, acc: SortedMap[ShortChannelId, (Long, Long)]): SortedMap[ShortChannelId, (Long, Long)] = { - val check = readBytes(buffer, input) - if (check <= 0) acc else { - val id = ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) - val ts1 = Protocol.uint32(buffer.drop(8), ByteOrder.BIG_ENDIAN) - val ts2 = Protocol.uint32(buffer.drop(12), ByteOrder.BIG_ENDIAN) - loop(input, acc + (id -> (ts1, ts2))) - } - } - - def readAll() = { - val bis = new ByteArrayInputStream(data.tail.toArray) - val input = format match { - case UNCOMPRESSED_FORMAT => bis - case ZLIB_FORMAT => new InflaterInputStream(bis) - } - try { - (format, loop(input, SortedMap.empty[ShortChannelId, (Long, Long)])) - } - finally { - input.close() - } - } - - readAll() - } - } -} - -case class ShortChannelIdAndFlag(id: ShortChannelId, flag: Byte) extends Ordered[ShortChannelIdAndFlag] { - override def compare(that: ShortChannelIdAndFlag): Int = this.id.compare(that.id) -} - -case class ShortChannelIdAndFlagsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndFlags: BinaryData) - -object ShortChannelIdAndFlagsBlock { - /** - * Compressed a sequence of *sorted* short channel id. - * - * @param shortChannelIds must be sorted beforehand - * @return a sequence of encoded short channel ids - */ - def encode(firstBlockIn: Long, numBlocksIn: Long, - shortChannelIds: SortedSet[ShortChannelId], flag: ShortChannelId => Byte, - format: Byte): List[ShortChannelIdAndFlagsBlock] = { - if (shortChannelIds.isEmpty) { - // special case: reply with an "empty" block - List(ShortChannelIdAndFlagsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) - } else { - // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { - case UNCOMPRESSED_FORMAT => 2000 - case ZLIB_FORMAT => 5000 // TODO: do something less simplistic... - } - shortChannelIds.grouped(count).map(ids => { - val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { - val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight - val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 - (firstBlock, numBlocks) - } - val encoded = encodeSingle(ids, flag, format) - ShortChannelIdAndFlagsBlock(firstBlock, numBlocks, encoded) - }).toList - } - } - - def encodeSingle(shortChannelIds: Iterable[ShortChannelId], flag: ShortChannelId => Byte, format: Byte): BinaryData = { - val bos = new ByteArrayOutputStream() - bos.write(format) - val out = format match { - case UNCOMPRESSED_FORMAT => bos - case ZLIB_FORMAT => new DeflaterOutputStream(bos) - } - shortChannelIds.foreach(id => { - Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN) - Protocol.writeUInt8(flag(id), out) - }) - out.close() - bos.toByteArray - } - - def encodeSingle(shortChannelIdAndFlags: SortedSet[ShortChannelIdAndFlag], format: Byte): BinaryData = { - val bos = new ByteArrayOutputStream() - bos.write(format) - val out = format match { - case UNCOMPRESSED_FORMAT => bos - case ZLIB_FORMAT => new DeflaterOutputStream(bos) - } - shortChannelIdAndFlags.foreach(value => { - Protocol.writeUInt64(value.id.toLong, out, ByteOrder.BIG_ENDIAN) - Protocol.writeUInt8(value.flag, out) - }) - out.close() - bos.toByteArray - } - - /** - * Decompress a zipped sequence of sorted [short channel id | timestamp1 | timestamp2 ] values. - * - * @param data - * @return a sorted map of short channel id -> timestamps - */ - def decode(data: BinaryData): (Byte, SortedMap[ShortChannelId, Byte]) = { - val format = data.head - if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, Byte]) else { - val buffer = new Array[Byte](9) - - // read until there's nothing left - @tailrec - def loop(input: InputStream, acc: SortedMap[ShortChannelId, Byte]): SortedMap[ShortChannelId, Byte] = { - val check = readBytes(buffer, input) - if (check <= 0) acc else { - val id = ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) - val flag = buffer(8) - loop(input, acc + (id -> flag)) - } - } - - def readAll() = { - val bis = new ByteArrayInputStream(data.tail.toArray) - val input = format match { - case UNCOMPRESSED_FORMAT => bis - case ZLIB_FORMAT => new InflaterInputStream(bis) - } - try { - (format, loop(input, SortedMap.empty[ShortChannelId, Byte])) - } - finally { - input.close() - } - } - - readAll() - } - } -} - diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala deleted file mode 100644 index 452981d7c1..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ShortChannelIdsBlock.scala +++ /dev/null @@ -1,99 +0,0 @@ -package fr.acinq.eclair.router - -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} -import java.nio.ByteOrder -import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream} - -import fr.acinq.bitcoin.{BinaryData, Protocol} -import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.router.ChannelRangeQueries.{UNCOMPRESSED_FORMAT, ZLIB_FORMAT, readBytes} - -import scala.annotation.tailrec -import scala.collection.SortedSet - -case class ShortChannelIdsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIds: BinaryData) - -object ShortChannelIdsBlock { - /** - * Compressed a sequence of *sorted* short channel id. - * - * @param shortChannelIds must be sorted beforehand - * @return a sequence of short channel id blocks - */ - def encode(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false): List[ShortChannelIdsBlock] = { - if (shortChannelIds.isEmpty) { - // special case: reply with an "empty" block - List(ShortChannelIdsBlock(firstBlockIn, numBlocksIn, BinaryData("00"))) - } else { - // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { - case UNCOMPRESSED_FORMAT => 7000 - case ZLIB_FORMAT => 12000 // TODO: do something less simplistic... - } - shortChannelIds.grouped(count).map(ids => { - val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { - val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight - val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 - (firstBlock, numBlocks) - } - val encoded = encodeSingle(ids, format, useGzip) - ShortChannelIdsBlock(firstBlock, numBlocks, encoded) - }).toList - } - } - - def encodeSingle(shortChannelIds: Iterable[ShortChannelId], format: Byte, useGzip: Boolean): BinaryData = { - val bos = new ByteArrayOutputStream() - bos.write(format) - val out = format match { - case UNCOMPRESSED_FORMAT => bos - case ZLIB_FORMAT => if (useGzip) new GZIPOutputStream(bos) else new DeflaterOutputStream(bos) - } - shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN)) - out.close() - bos.toByteArray - } - - /** - * Decompress a zipped sequence of sorted short channel ids. - * - * @param data - * @return a sorted set of short channel ids - */ - def decode(data: BinaryData): (Byte, SortedSet[ShortChannelId], Boolean) = { - val format = data.head - if (data.tail.isEmpty) (format, SortedSet.empty[ShortChannelId], false) else { - val buffer = new Array[Byte](8) - - // read until there's nothing left - @tailrec - def loop(input: InputStream, acc: SortedSet[ShortChannelId]): SortedSet[ShortChannelId] = { - val check = readBytes(buffer, input) - if (check <= 0) acc else loop(input, acc + ShortChannelId(Protocol.uint64(buffer, ByteOrder.BIG_ENDIAN))) - } - - def readAll(useGzip: Boolean) = { - val bis = new ByteArrayInputStream(data.tail.toArray) - val input = format match { - case UNCOMPRESSED_FORMAT => bis - case ZLIB_FORMAT if useGzip => new GZIPInputStream(bis) - case ZLIB_FORMAT => new InflaterInputStream(bis) - } - try { - (format, loop(input, SortedSet.empty[ShortChannelId]), useGzip) - } - finally { - input.close() - } - } - - try { - readAll(useGzip = false) - } - catch { - case _: Throwable if format == ZLIB_FORMAT => readAll(useGzip = true) - } - } - } -} - 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 dd3953935b..ba41a33135 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 @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire import java.math.BigInteger import java.net.{Inet4Address, Inet6Address, InetAddress} + import com.google.common.cache.{CacheBuilder, CacheLoader} import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{BinaryData, Crypto} @@ -308,9 +309,14 @@ object LightningMessageCodecs { ("signature" | signature) :: channelUpdateWitnessCodec).as[ChannelUpdate] + val encodedShortChannelIdsCodec: Codec[EncodedShortChannelIds] = + discriminated[EncodedShortChannelIds].by(byte) + .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) + .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIds(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) + val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( ("chainHash" | binarydata(32)) :: - ("data" | varsizebinarydata) + ("data" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) ).as[QueryShortChannelIds] val replyShortChanelIdsEndCodec: Codec[ReplyShortChannelIdsEnd] = ( @@ -329,7 +335,7 @@ object LightningMessageCodecs { ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: ("complete" | byte) :: - ("data" | varsizebinarydata) + ("data" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) ).as[ReplyChannelRange] val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( @@ -341,7 +347,7 @@ object LightningMessageCodecs { val queryShortChannelIdsDeprecatedCodec: Codec[QueryShortChannelIdsDeprecated] = ( ("chainHash" | binarydata(32)) :: ("flag" | byte) :: - ("data" | varsizebinarydata) + ("data" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) ).as[QueryShortChannelIdsDeprecated] val replyShortChanelIdsEndDeprecatedCodec: Codec[ReplyShortChannelIdsEndDeprecated] = ( @@ -355,17 +361,37 @@ object LightningMessageCodecs { ("numberOfBlocks" | uint32) ).as[QueryChannelRangeDeprecated] + val shortChannelIdWithTimestampCodec: Codec[ShortChannelIdWithTimestamp] = ( + ("shortChannelId" | shortchannelid) :: + ("timestamp" | uint32) + ).as[ShortChannelIdWithTimestamp] + + val channelRangeWithTimestampDataCodec: Codec[EncodedShortChannelIdsWithTimestamp] = + discriminated[EncodedShortChannelIdsWithTimestamp].by(byte) + .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortChannelIdWithTimestampCodec)).as[EncodedShortChannelIdsWithTimestamp]) + .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIdsWithTimestamp(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortChannelIdWithTimestampCodec))).as[EncodedShortChannelIdsWithTimestamp]) + val replyChannelRangeDeprecatedCodec: Codec[ReplyChannelRangeDeprecated] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: ("complete" | byte) :: - ("data" | varsizebinarydata) + ("data" | variableSizeBytes(uint16, channelRangeWithTimestampDataCodec)) ).as[ReplyChannelRangeDeprecated] + val shortChannelIdAndFlagCodec: Codec[ShortChannelIdAndFlag] = ( + ("shortChannelId" | shortchannelid) :: + ("flag" | byte) + ).as[ShortChannelIdAndFlag] + + val encodedShortChannelIdsAndFlagCodec: Codec[EncodedShortChannelIdsAndFlag] = + discriminated[EncodedShortChannelIdsAndFlag].by(byte) + .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIdsAndFlag(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortChannelIdAndFlagCodec)).as[EncodedShortChannelIdsAndFlag]) + .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIdsAndFlag(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortChannelIdAndFlagCodec))).as[EncodedShortChannelIdsAndFlag]) + val queryShortChannelIdsWithFlagsCodec: Codec[QueryShortChannelIdsWithFlags] = ( ("chainHash" | binarydata(32)) :: - ("data" | varsizebinarydata) + ("data" | variableSizeBytes(uint16, encodedShortChannelIdsAndFlagCodec)) ).as[QueryShortChannelIdsWithFlags] val replyShortChannelIdsWithFlagsEndCodec: Codec[ReplyShortChannelIdsWithFlagsEnd] = ( @@ -373,38 +399,24 @@ object LightningMessageCodecs { ("complete" | byte) ).as[ReplyShortChannelIdsWithFlagsEnd] - val queryChannelRangeWithTimestampsCodec: Codec[QueryChannelRangeWithTimestamps] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) - ).as[QueryChannelRangeWithTimestamps] - - val replyChannelRangeWithTimestampsCodec: Codec[ReplyChannelRangeWithTimestamps] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) :: - ("complete" | byte) :: - ("data" | varsizebinarydata) - ).as[ReplyChannelRangeWithTimestamps] - val queryChannelRangeWithChecksumsCodec: Codec[QueryChannelRangeWithChecksums] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) ).as[QueryChannelRangeWithChecksums] - val channelDigestInfoCodec: Codec[ChannelDigestInfo] = ( + val shortChannelIdWithChecksumsCodec: Codec[ShortChannelIdWithChecksums] = ( ("shortChannelId" | shortchannelid) :: ("timestamp1" | uint32) :: ("timestamp2" | uint32) :: ("checksum1" | uint32) :: ("checksum2" | uint32) - ).as[ChannelDigestInfo] + ).as[ShortChannelIdWithChecksums] - val channelRangeWithChecksumsDataCodec: Codec[ChannelRangeWithChecksumsData] = - discriminated[ChannelRangeWithChecksumsData].by(byte) - .\(EncodingTypes.UNCOMPRESSED) { case a@ChannelRangeWithChecksumsData(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(channelDigestInfoCodec)).as[ChannelRangeWithChecksumsData]) - .\(EncodingTypes.COMPRESSED_ZLIB) { case a@ChannelRangeWithChecksumsData(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(channelDigestInfoCodec))).as[ChannelRangeWithChecksumsData]) + val channelRangeWithChecksumsDataCodec: Codec[EncodedShortChannelIdsWithChecksums] = + discriminated[EncodedShortChannelIdsWithChecksums].by(byte) + .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortChannelIdWithChecksumsCodec)).as[EncodedShortChannelIdsWithChecksums]) + .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIdsWithChecksums(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortChannelIdWithChecksumsCodec))).as[EncodedShortChannelIdsWithChecksums]) val replyChannelRangeWithChecksumsCodec: Codec[ReplyChannelRangeWithChecksums] = ( ("chainHash" | binarydata(32)) :: @@ -449,10 +461,8 @@ object LightningMessageCodecs { .typecase(1004, replyChannelRangeDeprecatedCodec) .typecase(1011, queryShortChannelIdsWithFlagsCodec) .typecase(1012, replyShortChannelIdsWithFlagsEndCodec) - .typecase(1013, queryChannelRangeWithTimestampsCodec) - .typecase(1014, replyChannelRangeWithTimestampsCodec) - .typecase(1017, queryChannelRangeWithChecksumsCodec) - .typecase(1018, replyChannelRangeWithChecksumsCodec) + .typecase(1013, queryChannelRangeWithChecksumsCodec) + .typecase(1014, replyChannelRangeWithChecksumsCodec) /** * A codec that caches serialized routing messages 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 c90c2826c1..529e33800a 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 @@ -209,13 +209,16 @@ case class PerHopPayload(shortChannelId: ShortChannelId, // BOLT 1.0 channel queries -/** - * - * @param chainHash chain hash - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded - */ +case object EncodingTypes { + val UNCOMPRESSED: Byte = 0 + val COMPRESSED_ZLIB: Byte = 1 +} + +case class EncodedShortChannelIds(encoding: Byte, + array: List[ShortChannelId]) + case class QueryShortChannelIds(chainHash: BinaryData, - data: BinaryData) extends RoutingMessage with HasChainHash + data: EncodedShortChannelIds) extends RoutingMessage with HasChainHash case class ReplyShortChannelIdsEnd(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash @@ -228,7 +231,7 @@ case class ReplyChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash + data: EncodedShortChannelIds) extends RoutingMessage with HasChainHash case class GossipTimestampFilter(chainHash: BinaryData, firstTimestamp: Long, @@ -236,15 +239,21 @@ case class GossipTimestampFilter(chainHash: BinaryData, // prototype queries, used by eclair only, to be removed asap -/** - * - * @param chainHash chain hash - * @param flag if flag == 1, don't send back channel announcements - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded - */ +case object FlagTypes { + val INCLUDE_CHANNEL_UPDATE_1: Byte = 1 + val INCLUDE_CHANNEL_UPDATE_2: Byte = 2 + val INCLUDE_ANNOUNCEMENT: Byte = 4 + + def includeAnnouncement(flag: Byte) = (flag & FlagTypes.INCLUDE_ANNOUNCEMENT) != 0 + + def includeUpdate1(flag: Byte) = (flag & FlagTypes.INCLUDE_CHANNEL_UPDATE_1) != 0 + + def includeUpdate2(flag: Byte) = (flag & FlagTypes.INCLUDE_CHANNEL_UPDATE_2) != 0 +} + case class QueryShortChannelIdsDeprecated(chainHash: BinaryData, flag: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash + data: EncodedShortChannelIds) extends RoutingMessage with HasChainHash case class ReplyShortChannelIdsEndDeprecated(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash @@ -253,60 +262,49 @@ case class QueryChannelRangeDeprecated(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash -/** - * - * @param chainHash chain hash - * @param firstBlockNum first block that is found in data - * @param numberOfBlocks number of blocks spanned by data - * @param complete - * @param data prefix + list of (short channel id + timestamp) values, where prefix specifies how the list is encoded - */ +case class ShortChannelIdWithTimestamp(shortChannelId: ShortChannelId, + timestamp: Long) + +case class EncodedShortChannelIdsWithTimestamp(encoding: Byte, + array: List[ShortChannelIdWithTimestamp]) + case class ReplyChannelRangeDeprecated(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash + data: EncodedShortChannelIdsWithTimestamp) extends RoutingMessage with HasChainHash // proposal for BOLT 1.1 channel queries +case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, + flag: Byte) + +case class EncodedShortChannelIdsAndFlag(encoding: Byte, + array: List[ShortChannelIdAndFlag]) + case class QueryShortChannelIdsWithFlags(chainHash: BinaryData, - data: BinaryData) extends RoutingMessage with HasChainHash + data: EncodedShortChannelIdsAndFlag) extends RoutingMessage with HasChainHash case class ReplyShortChannelIdsWithFlagsEnd(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash -case class QueryChannelRangeWithTimestamps(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash - -case class ReplyChannelRangeWithTimestamps(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: BinaryData) extends RoutingMessage with HasChainHash - // 2nd proposal with checksums case class QueryChannelRangeWithChecksums(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long) extends RoutingMessage with HasChainHash -case object EncodingTypes { - val UNCOMPRESSED: Byte = 0 - val COMPRESSED_ZLIB: Byte = 1 -} - -case class ChannelDigestInfo(shortChannelId: ShortChannelId, - timestamp1: Long, - timestamp2: Long, - checksum1: Long, - checksum2: Long) +case class ShortChannelIdWithChecksums(shortChannelId: ShortChannelId, + timestamp1: Long, + timestamp2: Long, + checksum1: Long, + checksum2: Long) -case class ChannelRangeWithChecksumsData(encoding: Byte, - array: List[ChannelDigestInfo]) +case class EncodedShortChannelIdsWithChecksums(encoding: Byte, + array: List[ShortChannelIdWithChecksums]) case class ReplyChannelRangeWithChecksums(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, - data: ChannelRangeWithChecksumsData) extends RoutingMessage with HasChainHash \ No newline at end of file + data: EncodedShortChannelIdsWithChecksums) extends RoutingMessage with HasChainHash \ No newline at end of file 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 b4956c73c0..d4d33d4e3a 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 @@ -10,8 +10,8 @@ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements, SendPing} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast, ShortChannelIdsBlock} -import fr.acinq.eclair.wire.{Error, Ping, Pong} +import fr.acinq.eclair.router.{ChannelRangeQueriesSpec, Rebroadcast} +import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingTypes, Error, FlagTypes, Ping, Pong} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, randomBytes, wire} import org.scalatest.Outcome @@ -134,7 +134,7 @@ class PeerSpec extends TestkitBaseClass { val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) - val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, ShortChannelIdsBlock.encodeSingle(Seq(ShortChannelId(42000)), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42000)))) // make sure that routing messages go through for (ann <- channels ++ updates) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index a9aa319ce6..14a3acd08b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -17,7 +17,7 @@ class ChannelRangeQueriesSpec extends FunSuite { val timestamp = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap val timestamps = shortChannelIds.map(id => id -> (random.nextInt(400000).toLong, random.nextInt(400000).toLong)).toMap - test("create `reply_channel_range` messages (uncompressed format)") { + /*test("create `reply_channel_range` messages (uncompressed format)") { val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) var decoded = Set.empty[ShortChannelId] @@ -92,7 +92,7 @@ class ChannelRangeQueriesSpec extends FunSuite { replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) shortChannelIds.foreach(id => timestamp(id) == decoded(id)) - } + }*/ } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala index 625737e153..edde670551 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala @@ -13,7 +13,7 @@ class ChannelRangeQueriesWithTimestampsSpec extends FunSuite { val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds val timestamps = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap - test("create `reply_channel_range_ex` messages (uncompressed format)") { + /*test("create `reply_channel_range_ex` messages (uncompressed format)") { val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) var decoded = SortedMap.empty[ShortChannelId, Long] @@ -29,5 +29,5 @@ class ChannelRangeQueriesWithTimestampsSpec extends FunSuite { replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) assert(decoded.keySet == shortChannelIds) shortChannelIds.foreach(id => timestamps(id) == decoded(id)) - } + }*/ } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 5d7af4f3f6..39452522b6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -259,6 +259,6 @@ class RouterSpec extends BaseRouterSpec { val transport = TestProbe() probe.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, update1)) val query = transport.expectMsgType[QueryShortChannelIds] - assert(ShortChannelIdsBlock.decode(query.data)._2 == SortedSet(channelId)) + assert(query.data.array == List(channelId)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala new file mode 100644 index 0000000000..a368e0f443 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala @@ -0,0 +1,82 @@ +package fr.acinq.eclair.router + +import akka.actor.ActorSystem +import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import fr.acinq.eclair._ +import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.wire._ +import org.scalatest.FunSuiteLike + +import scala.collection.immutable.TreeMap +import scala.concurrent.duration._ + + +class RoutingSyncDeprecatedSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { + import RoutingSyncSpec.makeFakeRoutingInfo + + test("handle channel range extended queries") { + val params = TestConstants.Alice.nodeParams + val router = TestFSMRef(new Router(params, TestProbe().ref)) + val transport = TestProbe() + val sender = TestProbe() + sender.ignoreMsg { case _: TransportHandler.ReadAck => true } + val remoteNodeId = TestConstants.Bob.nodeParams.nodeId + + // ask router to send a channel range query + sender.send(router, SendChannelQueryDeprecated(remoteNodeId, sender.ref)) + val QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeDeprecated] + sender.expectMsgType[GossipTimestampFilter] + + + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) + val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap + val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } + val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => + val desc = Router.getDesc(u, initChannels(u.shortChannelId)) + (desc) -> u + }.toMap + + // split our anwser in 3 blocks + val block1 = ReplyChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList.map(id => ShortChannelIdWithTimestamp(id, Router.getTimestamp(initChannels, initChannelUpdates)(id))))) + val block2 = ReplyChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList.map(id => ShortChannelIdWithTimestamp(id, Router.getTimestamp(initChannels, initChannelUpdates)(id))))) + + // send first block + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) + // router should ask for our first block of ids + assert(transport.expectMsgType[QueryShortChannelIdsDeprecated] === QueryShortChannelIdsDeprecated(chainHash, (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte, EncodedShortChannelIds(block1.data.encoding, block1.data.array.map(_.shortChannelId)))) + + // send second block + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block2)) + + // router should not ask for more ids, it already has a pending query ! + sender.expectNoMsg(1 second) + + // send the first 50 items + block1.data.array.map(_.shortChannelId).take(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) + }) + + sender.expectNoMsg(1 second) + + // send the last 50 items + block1.data.array.map(_.shortChannelId).drop(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) + }) + + // during that time, router should not have asked for more ids, it already has a pending query ! + transport.expectNoMsg(200 millis) + + // now send our ReplyShortChannelIdsEnd message + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEndDeprecated(chainHash, 1.toByte))) + + // router should ask for our second block of ids + assert(transport.expectMsgType[QueryShortChannelIdsDeprecated] === QueryShortChannelIdsDeprecated(chainHash, (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte, EncodedShortChannelIds(block1.data.encoding, block2.data.array.map(_.shortChannelId)))) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala deleted file mode 100644 index 8cb1421a74..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncProtoSpec.scala +++ /dev/null @@ -1,83 +0,0 @@ -package fr.acinq.eclair.router - -import akka.actor.ActorSystem -import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.eclair._ -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.wire._ -import org.scalatest.FunSuiteLike - -import scala.collection.immutable.TreeMap -import scala.concurrent.duration._ - - -class RoutingSyncProtoSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { - import RoutingSyncSpec.makeFakeRoutingInfo - - test("handle chanel range extended queries") { - val params = TestConstants.Alice.nodeParams - val router = TestFSMRef(new Router(params, TestProbe().ref)) - val sender = TestProbe() - sender.ignoreMsg { case _: TransportHandler.ReadAck => true } - val remoteNodeId = TestConstants.Bob.nodeParams.nodeId - - // ask router to send a channel range query - sender.send(router, SendChannelQueryDeprecated(remoteNodeId, sender.ref)) - val QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeDeprecated] - sender.expectMsgType[GossipTimestampFilter] - - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => - val desc = Router.getDesc(u, initChannels(u.shortChannelId)) - (desc) -> u - }.toMap - - // split our anwser in 3 blocks - val List(block1) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block2) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block3) = ShortChannelIdAndTimestampBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - - // send first block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeDeprecated(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) - // router should ask for our first block of ids - val QueryShortChannelIdsDeprecated(_, _, data1) = sender.expectMsgType[QueryShortChannelIdsDeprecated] - val (_, shortChannelIds1, false) = ShortChannelIdsBlock.decode(data1) - assert(shortChannelIds1 == shortChannelIds.take(100)) - - // send second block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeDeprecated(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) - - // router should not ask for more ids, it already has a pending query ! - sender.expectNoMsg(1 second) - - // send the first 50 items - shortChannelIds1.take(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // send the last 50 items - shortChannelIds1.drop(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndDeprecated(chainHash, 1.toByte))) - - // router should ask for our second block of ids - val QueryShortChannelIdsDeprecated(_, _, data2) = sender.expectMsgType[QueryShortChannelIdsDeprecated] - val (_, shortChannelIds2, false) = ShortChannelIdsBlock.decode(data2) - assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index ea1e73c81f..60a3e4bf1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -37,30 +37,32 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.expectMsgType[GossipTimestampFilter] // split our answer in 3 blocks - val List(block1) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block2) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block3) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList)) + val block2 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList)) // send first block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) // router should ask for our first block of ids - val QueryShortChannelIds(_, data1) = transport.expectMsgType[QueryShortChannelIds] - val (_, shortChannelIds1, false) = ShortChannelIdsBlock.decode(data1) - assert(shortChannelIds1 == shortChannelIds.take(100)) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.data)) // send second block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIds))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block2)) + + // router should not ask for more ids, it already has a pending query ! + sender.expectNoMsg(1 second) // send the first 50 items - shortChannelIds1.take(50).foreach(id => { + block1.data.array.take(50).foreach(id => { val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) }) + sender.expectNoMsg(1 second) + // send the last 50 items - shortChannelIds1.drop(50).foreach(id => { + block1.data.array.drop(50).foreach(id => { val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) @@ -74,9 +76,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) // router should ask for our second block of ids - val QueryShortChannelIds(_, data2) = transport.expectMsgType[QueryShortChannelIds] - val (_, shortChannelIds2, false) = ShortChannelIdsBlock.decode(data2) - assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block2.data)) } test("reset sync state on reconnection") { @@ -92,16 +92,16 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - val List(block1) = ShortChannelIdsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList)) // send first block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) // router should ask for our first block of ids - val QueryShortChannelIds(_, data1) = transport.expectMsgType[QueryShortChannelIds] - // router should think that it is mssing 100 channels + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.data)) + // router should think that it is missing 100 channels, in one request val Some(sync) = router.stateData.sync.get(remoteNodeId) - assert(sync.totalMissingCount == 100) + assert(sync.total == 1) // simulate a re-connection sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala new file mode 100644 index 0000000000..d7b8753b02 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala @@ -0,0 +1,157 @@ +package fr.acinq.eclair.router + +import akka.actor.{Actor, ActorSystem, Props} +import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult} +import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire._ +import org.scalatest.FunSuiteLike + +import scala.collection.immutable.TreeMap +import scala.concurrent.duration._ + + +class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { + + import RoutingSyncSpec.makeFakeRoutingInfo + + val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) + val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap + val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } + val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => + val desc = Router.getDesc(u, initChannels(u.shortChannelId)) + (desc) -> u + }.toMap + + class FakeWatcher extends Actor { + override def receive: Receive = { + case ValidateRequest(c) => + val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) + val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) + val fakeFundingTx = Transaction( + version = 2, + txIn = Seq.empty[TxIn], + txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format + lockTime = 0) + sender ! ValidateResult(c, Some(fakeFundingTx), true, None) + } + } + + test("handle channel range extended (full sync)") { + val params = TestConstants.Alice.nodeParams + val watcher = system.actorOf(Props(new FakeWatcher())) + val router = TestFSMRef(new Router(params, watcher)) + val transport = TestProbe() + val sender = TestProbe() + sender.ignoreMsg { case _: TransportHandler.ReadAck => true } + val remoteNodeId = TestConstants.Bob.nodeParams.nodeId + + // ask router to send a channel range query + sender.send(router, SendChannelQueryWithChecksums(remoteNodeId, sender.ref)) + val QueryChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeWithChecksums] + sender.expectMsgType[GossipTimestampFilter] + + // send back all our ids and timestamps + val block = ReplyChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, initChannelUpdates)))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block)) + + // router should ask for our first block of ids + val shortChannelIdAndFlags = block.data.array.map(info => ShortChannelIdAndFlag(info.shortChannelId, (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte)) + val shortChannelIdAndFlags1 = shortChannelIdAndFlags.take(Router.SHORTID_WINDOW) + assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags1))) + + // send the first 50 items + shortChannelIdAndFlags1.take(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) + }) + sender.expectNoMsg(1 second) + + // send the last 50 items + shortChannelIdAndFlags1.drop(50).foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) + }) + sender.expectNoMsg(1 second) + + // now send our ReplyShortChannelIdsEnd message + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + + // router should ask for our second block of ids + val shortChannelIdAndFlags2 = shortChannelIdAndFlags.drop(Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) + assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags2))) + + // send block #2 + shortChannelIdAndFlags2.foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) + }) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + + // router should ask for our third block of ids + val shortChannelIdAndFlags3 = shortChannelIdAndFlags.drop(2 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) + assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags3))) + + // send block #3 + shortChannelIdAndFlags3.foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) + }) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + + // router should ask for our fourth block of ids + val shortChannelIdAndFlags4 = shortChannelIdAndFlags.drop(3 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) + assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags4))) + + // send block #4 + shortChannelIdAndFlags4.foreach(id => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) + }) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + + awaitCond({ + router.stateData.channels == initChannels + }, max = 30 seconds, interval = 500 millis) + + + val updatedIds = shortChannelIds.drop(100).take(50) + val recentChannelUpdates = updatedIds.foldLeft(initChannelUpdates) { + case (updates, id) => + val desc = ChannelDesc(id, RoutingSyncSpec.priv1.publicKey, RoutingSyncSpec.priv2.publicKey) + val update = updates(desc) + val foo = Announcements.isNode1(update.channelFlags) + assert(foo) + val newUpdate = RoutingSyncSpec.makeNewerChannelUpdate(update) + updates.updated(desc, newUpdate) + } + // ask router to send a channel range query + sender.send(router, SendChannelQueryWithChecksums(remoteNodeId, sender.ref)) + sender.expectMsgType[QueryChannelRangeWithChecksums] + sender.expectMsgType[GossipTimestampFilter] + + // send back all our ids and timestamps + val block1 = ReplyChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, recentChannelUpdates)))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) + + // router should ask for our new channel updates + val shortChannelIdAndFlags5 = block1.data.array.map(info => ShortChannelIdAndFlag(info.shortChannelId, FlagTypes.INCLUDE_CHANNEL_UPDATE_1)).filter(info => updatedIds.contains(info.shortChannelId)) + assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags5))) + + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala deleted file mode 100644 index b75fc540df..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithTimestampsSpec.scala +++ /dev/null @@ -1,161 +0,0 @@ -package fr.acinq.eclair.router - -import akka.actor.{Actor, ActorSystem, Props} -import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Satoshi, Script, Transaction, TxIn, TxOut} -import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult} -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.wire._ -import org.scalatest.FunSuiteLike - -import scala.collection.immutable.TreeMap -import scala.concurrent.duration._ - - -class RoutingSyncWithTimestampsSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { - - import RoutingSyncSpec.makeFakeRoutingInfo - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => - val desc = Router.getDesc(u, initChannels(u.shortChannelId)) - (desc) -> u - }.toMap - - class FakeWatcher extends Actor { - override def receive: Receive = { - case ValidateRequest(c) => - val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) - val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) - val fakeFundingTx = Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format - lockTime = 0) - sender ! ValidateResult(c, Some(fakeFundingTx), true, None) - } - } - - test("handle channel range extended (full sync)") { - val params = TestConstants.Alice.nodeParams - val watcher = system.actorOf(Props(new FakeWatcher())) - val router = TestFSMRef(new Router(params, watcher)) - val sender = TestProbe() - sender.ignoreMsg { case _: TransportHandler.ReadAck => true } - val remoteNodeId = TestConstants.Bob.nodeParams.nodeId - - // ask router to send a channel range query - sender.send(router, SendChannelQueryWithTimestamps(remoteNodeId, sender.ref)) - val QueryChannelRangeWithTimestamps(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeWithTimestamps] - sender.expectMsgType[GossipTimestampFilter] - - // send back all our ids and timestamps - val List(block) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum, numberOfBlocks, shortChannelIds, Router.getTimestamps(initChannels, initChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps))) - - // router should ask for our first block of ids - val QueryShortChannelIdsWithFlags(_, data1) = sender.expectMsgType[QueryShortChannelIdsWithFlags] - val (_, shortChannelIdAndFlags1) = ShortChannelIdAndFlagsBlock.decode(data1) - assert(shortChannelIdAndFlags1.keySet == shortChannelIds.take(Router.SHORTID_WINDOW)) - assert(shortChannelIdAndFlags1.values.toSet == Set(ChannelRangeQueries.INCLUDE_ANNOUNCEMENT)) - - // send the first 50 items - shortChannelIdAndFlags1.take(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // send the last 50 items - shortChannelIdAndFlags1.drop(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) - - // router should ask for our second block of ids - val QueryShortChannelIdsWithFlags(_, data2) = sender.expectMsgType[QueryShortChannelIdsWithFlags] - val (_, shortChannelIdAndFlags2) = ShortChannelIdAndFlagsBlock.decode(data2) - assert(shortChannelIdAndFlags2.keySet == shortChannelIds.drop(Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) - - // send block #2 - shortChannelIdAndFlags2.foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) - - // router should ask for our third block of ids - val QueryShortChannelIdsWithFlags(_, data3) = sender.expectMsgType[QueryShortChannelIdsWithFlags] - val (_, shortChannelIdAndFlags3) = ShortChannelIdAndFlagsBlock.decode(data3) - assert(shortChannelIdAndFlags3.keySet == shortChannelIds.drop(2 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) - - // send block #3 - shortChannelIdAndFlags3.foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) - - // router should ask for our third block of ids - val QueryShortChannelIdsWithFlags(_, data4) = sender.expectMsgType[QueryShortChannelIdsWithFlags] - val (_, shortChannelIdAndFlags4) = ShortChannelIdAndFlagsBlock.decode(data4) - assert(shortChannelIdAndFlags4.keySet == shortChannelIds.drop(3 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW)) - // send block #4 - shortChannelIdAndFlags4.foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id._1) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) - - awaitCond({ - router.stateData.channels == initChannels - }, max = 30 seconds, interval = 500 millis) - - - val updatedIds = shortChannelIds.drop(100).take(50) - val recentChannelUpdates = updatedIds.foldLeft(initChannelUpdates) { - case (updates, id) => - val desc = ChannelDesc(id, RoutingSyncSpec.priv1.publicKey, RoutingSyncSpec.priv2.publicKey) - val update = updates(desc) - val foo = Announcements.isNode1(update.channelFlags) - assert(foo) - val newUpdate = RoutingSyncSpec.makeNewerChannelUpdate(update) - updates.updated(desc, newUpdate) - } - // ask router to send a channel range query - sender.send(router, SendChannelQueryWithTimestamps(remoteNodeId, sender.ref)) - val QueryChannelRangeWithTimestamps(_, firstBlockNum1, numberOfBlocks1) = sender.expectMsgType[QueryChannelRangeWithTimestamps] - sender.expectMsgType[GossipTimestampFilter] - - // send back all our ids and timestamps - val List(block1) = ShortChannelIdAndTimestampsBlock.encode(firstBlockNum1, numberOfBlocks1, shortChannelIds, Router.getTimestamps(initChannels, recentChannelUpdates), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeWithTimestamps(chainHash, block.firstBlock, block.numBlocks, 1, block1.shortChannelIdAndTimestamps))) - - // router should ask for our new channel updates - val QueryShortChannelIdsWithFlags(_, data5) = sender.expectMsgType[QueryShortChannelIdsWithFlags] - val (_, shortChannelIdAndFlags5) = ShortChannelIdAndFlagsBlock.decode(data5) - assert(shortChannelIdAndFlags5.keySet == updatedIds) - assert(shortChannelIdAndFlags5.values.toSet == Set(ChannelRangeQueries.INCLUDE_CHANNEL_UPDATE_1)) - - } -} 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 feaebfc41c..255eecea27 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 @@ -22,14 +22,15 @@ import com.google.common.net.InetAddresses import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.router.ChannelRangeQueries.{UNCOMPRESSED_FORMAT, ZLIB_FORMAT} -import fr.acinq.eclair.router.{Announcements, ShortChannelIdAndTimestampsBlock} +import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomBytes, randomKey} import org.scalatest.FunSuite import scodec.bits.{BitVector, ByteVector, HexStringSyntax} +import scodec.Codec +import scodec.codecs._ -import scala.collection.{SortedMap, SortedSet} +import scala.collection.SortedMap /** * Created by PM on 31/05/2016. @@ -226,9 +227,9 @@ class LightningMessageCodecsSpec extends FunSuite { val channel_update = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, 4, 5, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes(32), ShortChannelId(42), randomSignature, randomSignature) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, randomBytes(7515)) + val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676)))) val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, randomBytes(3200)) + val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676)))) val ping = Ping(100, BinaryData("01" * 10)) val pong = Pong(BinaryData("01" * 10)) val channel_reestablish = ChannelReestablish(randomBytes(32), 242842L, 42L) @@ -309,9 +310,6 @@ class LightningMessageCodecsSpec extends FunSuite { case class A(a: Byte, i: Int) - import scodec.Codec - import scodec.codecs._ - val codec: Codec[A] = discriminated[A].by(byte) .\(0) { case a@A(0, _) => a }((provide(0: Byte) :: uint8).as[A]) @@ -323,28 +321,7 @@ class LightningMessageCodecsSpec extends FunSuite { logToStdOut(codec).decode(bin2.require) } - test("basic channel range nonreg tests") { - - case class ChannelTimestampsInfo(shortChannelId: ShortChannelId, - node1Timestamp: Long, - node2Timestamp: Long) - - case class ChannelRangeWithTimestampsData(encoding: Byte, - array: List[ChannelTimestampsInfo]) - - import scodec.Codec - import scodec.codecs._ - - val channelTimestampsInfoCodec: Codec[ChannelTimestampsInfo] = ( - ("shortChannelId" | shortchannelid) :: - ("node1Timestamp" | uint32) :: - ("node2Timestamp" | uint32) - ).as[ChannelTimestampsInfo] - - val channelRangeWithTimestampsDataCodec: Codec[ChannelRangeWithTimestampsData] = - discriminated[ChannelRangeWithTimestampsData].by(byte) - .\(EncodingTypes.UNCOMPRESSED) { case a@ChannelRangeWithTimestampsData(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(channelTimestampsInfoCodec)).as[ChannelRangeWithTimestampsData]) - .\(EncodingTypes.COMPRESSED_ZLIB) { case a@ChannelRangeWithTimestampsData(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(channelTimestampsInfoCodec))).as[ChannelRangeWithTimestampsData]) + test("nonreg tests") { val channels = SortedMap( ShortChannelId(0xaa) -> (0x1000L, 0x1001L), @@ -352,23 +329,69 @@ class LightningMessageCodecsSpec extends FunSuite { ShortChannelId(0xcc) -> (0x3000L, 0x3001L) ) - for (encoding <- List(UNCOMPRESSED_FORMAT, ZLIB_FORMAT)) { - - val encodedManual = ShortChannelIdAndTimestampsBlock - .encode(0, 42, channels.keySet, id => channels(id), encoding) - .head - .shortChannelIdAndTimestamps + val chainHash = Block.RegtestGenesisBlock.hash + val firstBlockIn = 0xa0 + val numBlockIn = 0xff + + // val encodeManualOriginal = ShortChannelIdsBlock + // .encode(firstBlockIn, numBlockIn, channels.keySet, UNCOMPRESSED_FORMAT) + // .head + // .shortChannelIds + // val reply_channel_range_original_uncompressed = ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, encodeManualOriginal) + // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_original_uncompressed).require.toHex) + // + // val encodeManualOriginalCompressed = ShortChannelIdsBlock + // .encode(firstBlockIn, numBlockIn, channels.keySet, ZLIB_FORMAT) + // .head + // .shortChannelIds + // val reply_channel_range_original_compressed = ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, encodeManualOriginalCompressed) + // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_original_compressed).require.toHex) + // + // val encodeManualDeprecated = ShortChannelIdAndTimestampBlock + // .encode(firstBlockIn, numBlockIn, channels.keySet, id => channels(id)._1, UNCOMPRESSED_FORMAT) + // .head + // .shortChannelIdAndTimestamps + // val reply_channel_range_deprecated_uncompressed = ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, encodeManualDeprecated) + // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_deprecated_uncompressed).require.toHex) + // + // val encodeManualDeprecatedCompressed = ShortChannelIdAndTimestampBlock + // .encode(firstBlockIn, numBlockIn, channels.keySet, id => channels(id)._1, ZLIB_FORMAT) + // .head + // .shortChannelIdAndTimestamps + // val reply_channel_range_deprecated_compressed = ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, encodeManualDeprecatedCompressed) + // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_deprecated_compressed).require.toHex) + + // encoded ReplyChannelRange(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,0000000000000000aa00000000000000bb00000000000000cc) to Successful(BitVector(560 bits, #448413046)) + // 010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100190000000000000000aa00000000000000bb00000000000000cc + // encoded ReplyChannelRange(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,01789c6360008355108a6137943e030012c10232) to Successful(BitVector(520 bits, #-663753163)) + // 010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001401789c6360008355108a6137943e030012c10232 + // encoded ReplyChannelRangeDeprecated(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,0000000000000000aa0000100000000000000000bb0000200000000000000000cc00003000) to Successful(BitVector(656 bits, #1135811878)) + // 03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100250000000000000000aa0000100000000000000000bb0000200000000000000000cc00003000 + // encoded ReplyChannelRangeDeprecated(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,01789c63600083550c0c020c50b09b814101c63ec3c060c00000278d0292) to Successful(BitVector(600 bits, #-635546169)) + // 03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001e01789c63600083550c0c020c50b09b814101c63ec3c060c00000278d0292 + + val encodedManualOriginalBin = BinaryData("010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100190000000000000000aa00000000000000bb00000000000000cc") + val encodedManualOriginalCompressedBin = BinaryData("010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001401789c6360008355108a6137943e030012c10232") + val encodedManualDeprecatedBin = BinaryData("03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100250000000000000000aa0000100000000000000000bb0000200000000000000000cc00003000") + val encodedManualDeprecatedCompressedBin = BinaryData("03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001e01789c63600083550c0c020c50b09b814101c63ec3c060c00000278d0292") - val decodedScodec = channelRangeWithTimestampsDataCodec.decode(BitVector.apply(encodedManual.data)).require.value - assert(decodedScodec.encoding == encoding) - assert(decodedScodec.array == channels.toList.map(x => ChannelTimestampsInfo(x._1, x._2._1, x._2._2))) + val expected = Map( + encodedManualOriginalBin -> ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, channels.keySet.toList)), + encodedManualOriginalCompressedBin -> ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIds(EncodingTypes.COMPRESSED_ZLIB, channels.keySet.toList)), + encodedManualDeprecatedBin -> ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, channels.toList.map(i => ShortChannelIdWithTimestamp(i._1, i._2._1)))), + encodedManualDeprecatedCompressedBin -> ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.COMPRESSED_ZLIB, channels.toList.map(i => ShortChannelIdWithTimestamp(i._1, i._2._1)))) + ) - val encodedScodec = channelRangeWithTimestampsDataCodec.encode(decodedScodec).require - assert(BinaryData(encodedScodec.toByteArray) == encodedManual) + for ((bin, obj) <- expected) { + val decoded = lightningMessageCodec.decode(BitVector.apply(bin.data)).require.value + assert(decoded === obj) + val encoded = lightningMessageCodec.encode(decoded).require + assert(BinaryData(encoded.toByteArray) == bin) } } + } object LightningMessageCodecsSpec { From 5ffabe999f848b4da16b3071678c3896e2276496 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 21 Jan 2019 18:57:22 +0100 Subject: [PATCH 20/86] enable extended queries in flags --- eclair-core/src/main/resources/reference.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index f4d2fe4eb7..91c3eec358 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -44,7 +44,6 @@ eclair { global-features = "" local-features = "02808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_deprecated + option_channel_range_queries_extended - #local-features = "808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_deprecated override-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", From a16d3d76c7ee81b3b03964a31beec5c3fd88a45e Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 21 Jan 2019 18:57:49 +0100 Subject: [PATCH 21/86] only send updates if explicitely asked --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 03029625f1..776f9cbce6 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 @@ -627,14 +627,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => if (FlagTypes.includeAnnouncement(flag)) transport ! ca - if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) + if (FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) + if (FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } }) transport ! ReplyShortChannelIdsEndDeprecated(chainHash, 1) stay - // new extended query message: a list of [channel id + flag] case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsWithFlags(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) @@ -646,8 +645,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => if (FlagTypes.includeAnnouncement(flag)) transport ! ca - if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - if (FlagTypes.includeAnnouncement(flag) || FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) + if (FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) + if (FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } } } From 430823a5d76b63b8a05b53602dc6e701674d333b Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 21 Jan 2019 19:58:13 +0100 Subject: [PATCH 22/86] always return channel+updates in deprecated mode --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 50953c4c52..230be78c95 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 @@ -628,14 +628,15 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsDeprecated(chainHash, flag, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val shortChannelIds = data.array - log.info("received query_short_channel_ids_proto for {} channel announcements, flag={}", shortChannelIds.size, flag) + log.info("received query_short_channel_ids_proto for {} channel announcements, flag={} (NB: flag is ignored!!)", shortChannelIds.size, flag) shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => - if (FlagTypes.includeAnnouncement(flag)) transport ! ca - if (FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - if (FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) + // TODO: for backward compatibility we always return all data + transport ! ca + d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) + d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) } }) transport ! ReplyShortChannelIdsEndDeprecated(chainHash, 1) From fdbaf5d9ddd4ac9968979f5287d5e8033f6ea799 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 21 Jan 2019 22:38:58 +0100 Subject: [PATCH 23/86] fixed QueryChannelRange handler --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 230be78c95..ffdefefc13 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 @@ -515,7 +515,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) split(shortChannelIds) - .foreach(chunk => ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds))) + .foreach(chunk => transport ! ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds))) stay case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks)), d) => @@ -595,7 +595,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom ShortChannelIdAndFlag(channelInfo.shortChannelId, flag.toByte) } .filter(_.flag != 0) - log.info("received reply_channel_range_with_checksums, we're missing {} channel announcements/updates, format={}", shortChannelIdAndFlags.size, data.encoding) + val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) { + case ((c, u), ShortChannelIdAndFlag(_, flag)) => + val c1 = c + (if (FlagTypes.includeAnnouncement(flag)) 1 else 0) + val u1 = u + (if (FlagTypes.includeUpdate1(flag)) 1 else 0) + (if (FlagTypes.includeUpdate2(flag)) 1 else 0) + (c1, u1) + } + log.info("received reply_channel_range_with_checksums with {} channels, we're missing {} channel announcements and {} updates, format={}", data.array.size, channelCount, updatesCount, data.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) val replies = shortChannelIdAndFlags .grouped(SHORTID_WINDOW) From 21359e26a27670a4f234d2b1a53d54adf577a1b6 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 21 Jan 2019 22:54:19 +0100 Subject: [PATCH 24/86] fixed sync progress calculation --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 ffdefefc13..198c6ba1e2 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 @@ -77,8 +77,7 @@ case class Sync(pending: List[RoutingMessage], total: Int) { * @return returns a sync progress indicator (1 means fully synced) */ def progress: Double = { - val inflight = pending.size * 1.0 - if (total == 0) 1.0 else inflight / total + if (total == 0) 1.0 else ((1.0 - pending.size) / total) } } @@ -1017,7 +1016,7 @@ object Router { syncMap.get(remoteNodeId) match { case None => // we don't have a pending query with this peer, let's send it - (syncMap + (remoteNodeId -> Sync(rest, pending.size)), Some(head)) + (syncMap + (remoteNodeId -> Sync(rest, 1 + pending.size)), Some(head)) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state (syncMap + (remoteNodeId -> Sync(sync.pending ++ pending, sync.total + pending.size)), None) From 919bad71711731e2b74c397c39b3b754884855e1 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 22 Jan 2019 10:46:08 +0100 Subject: [PATCH 25/86] set feature bits 22/23 for extended channel queries --- eclair-core/src/main/resources/reference.conf | 2 +- eclair-core/src/main/scala/fr/acinq/eclair/Features.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index d2be9aab75..fe2659ec0a 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -43,7 +43,7 @@ eclair { node-color = "49daaa" global-features = "" - local-features = "02808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_deprecated + option_channel_range_queries_extended + local-features = "80808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_deprecated + option_channel_range_queries_extended override-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 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 95ef40cffc..94a4a7f5ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -39,8 +39,8 @@ object Features { val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_MANDATORY = 14 val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_OPTIONAL = 15 - val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY = 16 - val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL = 17 + val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY = 22 + val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL = 23 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) From 27c21a91dc46ecfcb8d3ccac45791acd0b0de411 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 22 Jan 2019 11:52:19 +0100 Subject: [PATCH 26/86] better logs + refactoring --- .../scala/fr/acinq/eclair/router/Router.scala | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) 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 198c6ba1e2..c7fc687052 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 @@ -598,7 +598,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case ((c, u), ShortChannelIdAndFlag(_, flag)) => val c1 = c + (if (FlagTypes.includeAnnouncement(flag)) 1 else 0) val u1 = u + (if (FlagTypes.includeUpdate1(flag)) 1 else 0) + (if (FlagTypes.includeUpdate2(flag)) 1 else 0) - (c1, u1) + (c1, u1) } log.info("received reply_channel_range_with_checksums with {} channels, we're missing {} channel announcements and {} updates, format={}", data.array.size, channelCount, updatesCount, data.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) @@ -615,54 +615,37 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // standard query message: a list of channel ids case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val shortChannelIds = data.array - log.info("received query_short_channel_ids for {} channel announcements", shortChannelIds.size) - shortChannelIds.foreach(shortChannelId => { - d.channels.get(shortChannelId) match { - case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) - case Some(ca) => - transport ! ca - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) - } - }) + val (channelCount, updatesCount) = handleChannelQuery(d, transport, data.array, + id = (id: ShortChannelId) => id, + sendChannel = (_: ShortChannelId) => true, + sendUpdate1 = (_: ShortChannelId) => true, + sendUpdate2 = (_: ShortChannelId) => true) + log.info("received query_short_channel_ids with {} items, sent back {} channels and {} updates", data.array.size, channelCount, updatesCount) transport ! ReplyShortChannelIdsEnd(chainHash, 1) stay // extended query message: a flag and a list of channel ids case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsDeprecated(chainHash, flag, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val shortChannelIds = data.array - log.info("received query_short_channel_ids_proto for {} channel announcements, flag={} (NB: flag is ignored!!)", shortChannelIds.size, flag) - shortChannelIds.foreach(shortChannelId => { - d.channels.get(shortChannelId) match { - case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) - case Some(ca) => - // TODO: for backward compatibility we always return all data - transport ! ca - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) - } - }) + // TODO: for backward compatibility we always return all data + val (channelCount, updatesCount) = handleChannelQuery(d, transport, data.array, + id = (id: ShortChannelId) => id, + sendChannel = (_: ShortChannelId) => true, + sendUpdate1 = (_: ShortChannelId) => true, + sendUpdate2 = (_: ShortChannelId) => true) + log.info("received query_short_channel_ids_deprecated with {} items, flag={} (ignored!!), sent back {} channels and {} updates", data.array.size, flag, channelCount, updatesCount) transport ! ReplyShortChannelIdsEndDeprecated(chainHash, 1) stay // new extended query message: a list of [channel id + flag] case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsWithFlags(chainHash, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val shortChannelIdAndFlags = data.array - log.info("received query_short_channel_ids_ex for {} channel announcements", shortChannelIdAndFlags.size) - shortChannelIdAndFlags.foreach { - case ShortChannelIdAndFlag(shortChannelId, flag) => { - d.channels.get(shortChannelId) match { - case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) - case Some(ca) => - if (FlagTypes.includeAnnouncement(flag)) transport ! ca - if (FlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - if (FlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) - } - } - } + val (channelCount, updatesCount) = handleChannelQuery(d, transport, data.array, + id = (item: ShortChannelIdAndFlag) => item.shortChannelId, + sendChannel = (item: ShortChannelIdAndFlag) => FlagTypes.includeAnnouncement(item.flag), + sendUpdate1 = (item: ShortChannelIdAndFlag) => FlagTypes.includeUpdate1(item.flag), + sendUpdate2 = (item: ShortChannelIdAndFlag) => FlagTypes.includeUpdate2(item.flag)) + log.info("received query_short_channel_ids_ex with {} items, sent back {} channels and {} updates", data.array.size, channelCount, updatesCount) transport ! ReplyShortChannelIdsWithFlagsEnd(chainHash, 1) stay @@ -850,6 +833,26 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom d.copy(sync = sync1) } + def handleChannelQuery[T](d: Data, transport: ActorRef, items: Iterable[T], id: T => ShortChannelId, sendChannel: T => Boolean, sendUpdate1: T => Boolean, sendUpdate2: T => Boolean): (Int, Int) = { + items.foldLeft((0, 0)) { + case ((c, u), item) => + var c1 = c + var u1 = u + val shortChannelId = id(item) + d.channels.get(shortChannelId) match { + case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) + case Some(ca) => + if (sendChannel(item)) { + transport ! ca + c1 = c1 + 1 + } + if (sendUpdate1(item)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } + if (sendUpdate2(item)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } + } + (c1, u1) + } + } + override def mdc(currentMessage: Any): MDC = currentMessage match { case SendChannelQuery(remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) case PeerRoutingMessage(_, remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) @@ -1001,11 +1004,11 @@ object Router { .grouped(2000) // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid .toList .map { group => - // NB: group is never empty - val firstBlock: Long = ShortChannelId.coordinates(group.head).blockHeight.toLong - val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong - ShortChannelIdsChunk(firstBlock, numBlocks, group.toList) - } + // NB: group is never empty + val firstBlock: Long = ShortChannelId.coordinates(group.head).blockHeight.toLong + val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong + ShortChannelIdsChunk(firstBlock, numBlocks, group.toList) + } } def updateSync(syncMap: Map[PublicKey, Sync], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Sync], Option[RoutingMessage]) = { @@ -1059,7 +1062,7 @@ object Router { val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges, numRoutes).toList match { case Nil => throw RouteNotFound - case route :: Nil if route.path.isEmpty => throw RouteNotFound + case route :: Nil if route.path.isEmpty => throw RouteNotFound case foundRoutes => foundRoutes } @@ -1067,7 +1070,7 @@ object Router { val minimumCost = foundRoutes.head.weight // routes paying at most minimumCost + 10% - val eligibleRoutes = foundRoutes.filter(_.weight <= (minimumCost + minimumCost * DEFAULT_ALLOWED_SPREAD).round) + val eligibleRoutes = foundRoutes.filter(_.weight <= (minimumCost + minimumCost * DEFAULT_ALLOWED_SPREAD).round) Random.shuffle(eligibleRoutes).head.path.map(graphEdgeToHop) } } From 3ba1f15b269c8c7b6bee2e8955c05d181bf0ea76 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 22 Jan 2019 13:59:33 +0100 Subject: [PATCH 27/86] removed temporary test --- .../eclair/wire/LightningMessageCodecsSpec.scala | 15 --------------- 1 file changed, 15 deletions(-) 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 255eecea27..7acae69baf 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 @@ -306,21 +306,6 @@ class LightningMessageCodecsSpec extends FunSuite { assert(bin === bin2) } - test("test TV codecs") { - - case class A(a: Byte, i: Int) - - val codec: Codec[A] = - discriminated[A].by(byte) - .\(0) { case a@A(0, _) => a }((provide(0: Byte) :: uint8).as[A]) - .\(1) { case a@A(1, _) => a }((provide(1: Byte) :: uint16).as[A]) - - val bin1 = logToStdOut(codec).encode(A(0, 42)) - logToStdOut(codec).decode(bin1.require) - val bin2 = logToStdOut(codec).encode(A(1, 42)) - logToStdOut(codec).decode(bin2.require) - } - test("nonreg tests") { val channels = SortedMap( From 428fd0a594013830491fa8657756361f8f152568 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 22 Jan 2019 14:14:42 +0100 Subject: [PATCH 28/86] fixed sync count --- eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c7fc687052..9ceef14f67 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 @@ -1019,7 +1019,7 @@ object Router { syncMap.get(remoteNodeId) match { case None => // we don't have a pending query with this peer, let's send it - (syncMap + (remoteNodeId -> Sync(rest, 1 + pending.size)), Some(head)) + (syncMap + (remoteNodeId -> Sync(rest, pending.size)), Some(head)) case Some(sync) => // we already have a pending query with this peer, add missing ids to our "sync" state (syncMap + (remoteNodeId -> Sync(sync.pending ++ pending, sync.total + pending.size)), None) From 366507e263a82c9f936f8e8b45e8c255d8d1ea67 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 22 Jan 2019 16:34:23 +0100 Subject: [PATCH 29/86] fixed numBlocks calculation --- eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9ceef14f67..de4165c7ca 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 @@ -1006,7 +1006,7 @@ object Router { .map { group => // NB: group is never empty val firstBlock: Long = ShortChannelId.coordinates(group.head).blockHeight.toLong - val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong + val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong - firstBlock + 1 ShortChannelIdsChunk(firstBlock, numBlocks, group.toList) } } From 7685c7d45b28841ef82428417d20e965a0302a6b Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 23 Jan 2019 18:38:15 +0100 Subject: [PATCH 30/86] don't request stale channel_updates --- .../scala/fr/acinq/eclair/router/Router.scala | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) 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 de4165c7ca..e82aa2f467 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 @@ -581,17 +581,21 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeWithChecksums(chainHash, _, _, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val shortChannelIdAndFlags = data.array - .map { channelInfo => + .map { theirInfo => var flag = 0 - if (d.channels.contains(channelInfo.shortChannelId)) { - val ourInfo = Router.getChannelDigestInfo(d.channels, d.updates)(channelInfo.shortChannelId) - if (ourInfo.timestamp1 < channelInfo.timestamp1 && ourInfo.checksum1 != channelInfo.checksum1) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 - if (ourInfo.timestamp2 < channelInfo.timestamp2 && ourInfo.checksum2 != channelInfo.checksum2) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_2 + if (d.channels.contains(theirInfo.shortChannelId)) { + val ourInfo = Router.getChannelDigestInfo(d.channels, d.updates)(theirInfo.shortChannelId) + // we request their channel_update if all those conditions are met: + // - it is more recent than ours + // - it is different from ours, or it is the same but ours is about to be stale + // - it is not stale itself + if (ourInfo.timestamp1 < theirInfo.timestamp1 && (ourInfo.checksum1 != theirInfo.checksum1 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp1)) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 + if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_2 } else { // we don't know this channel: we request everything flag = flag | FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2 } - ShortChannelIdAndFlag(channelInfo.shortChannelId, flag.toByte) + ShortChannelIdAndFlag(theirInfo.shortChannelId, flag.toByte) } .filter(_.flag != 0) val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) { @@ -887,11 +891,19 @@ object Router { def hasChannels(nodeId: PublicKey, channels: Iterable[ChannelAnnouncement]): Boolean = channels.exists(c => isRelatedTo(c, nodeId)) - def isStale(u: ChannelUpdate): Boolean = { + def isStale(u: ChannelUpdate): Boolean = isStale(u.timestamp) + + def isStale(timestamp: Long): Boolean = { // BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks (1209600 seconds)" // but we don't want to prune brand new channels for which we didn't yet receive a channel update val staleThresholdSeconds = Platform.currentTime / 1000 - 1209600 - u.timestamp < staleThresholdSeconds + timestamp < staleThresholdSeconds + } + + def isAlmostStale(timestamp: Long): Boolean = { + // we define almost stale as 2 weeks minus 4 days ( + val staleThresholdSeconds = Platform.currentTime / 1000 - 864000 + timestamp < staleThresholdSeconds } /** From 0d9f2851717a74a0d965ec96483ffb724b9da2b8 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 29 Jan 2019 11:38:34 +0100 Subject: [PATCH 31/86] cleaned function signature (removed unused args) --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 e82aa2f467..47159787f3 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 @@ -511,7 +511,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom sender ! TransportHandler.ReadAck(routingMessage) log.info("received {}", routingMessage) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) split(shortChannelIds) .foreach(chunk => transport ! ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds))) @@ -521,7 +521,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom sender ! TransportHandler.ReadAck(routingMessage) log.info("received {}", routingMessage) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) split(shortChannelIds) .foreach(chunk => transport ! ReplyChannelRangeDeprecated(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds.map(id => ShortChannelIdWithTimestamp(id, getTimestamp(d.channels, d.updates)(id)))))) @@ -531,7 +531,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom sender ! TransportHandler.ReadAck(routingMessage) log.info("received {}", routingMessage) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) split(shortChannelIds) .foreach(chunk => transport ! ReplyChannelRangeWithChecksums(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates))))) @@ -540,7 +540,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val theirShortChannelIds: SortedSet[ShortChannelId] = SortedSet(data.array: _*) - val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) val missing: SortedSet[ShortChannelId] = theirShortChannelIds -- ourShortChannelIds log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={}", missing.size, data.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) @@ -937,7 +937,7 @@ object Router { /** * Filters channels that we want to send to nodes asking for a channel range */ - def keep(firstBlockNum: Long, numberOfBlocks: Long, id: ShortChannelId, channels: Map[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate]): Boolean = { + def keep(firstBlockNum: Long, numberOfBlocks: Long, id: ShortChannelId): Boolean = { val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } From de866e2a0541ed2e50f4f6456655bde9a0b29917 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 29 Jan 2019 13:27:01 +0100 Subject: [PATCH 32/86] fixed sync progress calculation --- .../scala/fr/acinq/eclair/router/Router.scala | 28 ++++++++++--------- .../acinq/eclair/router/RoutingSyncSpec.scala | 20 +++++++++++++ 2 files changed, 35 insertions(+), 13 deletions(-) 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 47159787f3..b5a09611ba 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 @@ -70,16 +70,7 @@ case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterab case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) -case class Sync(pending: List[RoutingMessage], total: Int) { - - /** - * NB: progress is in terms of requests, not individual channels - * @return returns a sync progress indicator (1 means fully synced) - */ - def progress: Double = { - if (total == 0) 1.0 else ((1.0 - pending.size) / total) - } -} +case class Sync(pending: List[RoutingMessage], total: Int) case class Data(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[ShortChannelId, ChannelAnnouncement], @@ -942,12 +933,23 @@ object Router { height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } - def syncProgress(sync: Map[PublicKey, Sync]): SyncProgress = - if (sync.isEmpty) { + /** + * Returns overall progress on synchronization + * + * @param sync + * @return a sync progress indicator (1 means fully synced) + */ + def syncProgress(sync: Map[PublicKey, Sync]): SyncProgress = { + //NB: progress is in terms of requests, not individual channels + val (pending, total) = sync.foldLeft((0, 0)) { + case ((p, t), (_, sync)) => (p + sync.pending.size, t + sync.total) + } + if (total == 0) { SyncProgress(1) } else { - SyncProgress(sync.values.map(_.progress).sum / sync.values.size) + SyncProgress((total - pending) / (1.0 * total)) } + } /** * This method is used after a payment failed, and we want to exclude some nodes that we know are failing diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 60a3e4bf1e..9db0779e24 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -109,6 +109,26 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.expectMsgType[GossipTimestampFilter] assert(router.stateData.sync.get(remoteNodeId).isEmpty) } + + test("sync progress") { + + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42)))) + + val nodeidA = randomKey.publicKey + val nodeidB = randomKey.publicKey + + val (sync1, _) = Router.updateSync(Map.empty, nodeidA, List(req, req, req, req)) + assert(Router.syncProgress(sync1) == SyncProgress(0.25D)) + + val (sync2, _) = Router.updateSync(sync1, nodeidB, List(req, req, req, req, req, req, req, req, req, req, req, req)) + assert(Router.syncProgress(sync2) == SyncProgress(0.125D)) + + // let's assume we made some progress + val sync3 = sync2 + .updated(nodeidA, sync2(nodeidA).copy(pending = List(req))) + .updated(nodeidB, sync2(nodeidB).copy(pending = List(req))) + assert(Router.syncProgress(sync3) == SyncProgress(0.875D)) + } } From c068b9311941bcd7e059199770afd9170c2b28df Mon Sep 17 00:00:00 2001 From: pm47 Date: Sun, 10 Mar 2019 21:11:57 +0100 Subject: [PATCH 33/86] wip (tests not passing) --- .../main/scala/fr/acinq/eclair/Features.scala | 6 - .../main/scala/fr/acinq/eclair/io/Peer.scala | 22 +- .../scala/fr/acinq/eclair/router/Router.scala | 253 +++++------------- .../eclair/wire/LightningMessageCodecs.scala | 114 ++------ .../eclair/wire/LightningMessageTypes.scala | 104 +++---- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- .../router/ChannelRangeQueriesSpec.scala | 2 +- ...hannelRangeQueriesWithTimestampsSpec.scala | 33 --- .../fr/acinq/eclair/router/RouterSpec.scala | 2 +- .../router/RoutingSyncDeprecatedSpec.scala | 82 ------ .../acinq/eclair/router/RoutingSyncSpec.scala | 28 +- .../router/RoutingSyncWithChecksumsSpec.scala | 50 ++-- .../wire/LightningMessageCodecsSpec.scala | 78 +----- 13 files changed, 172 insertions(+), 604 deletions(-) delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala 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 94a4a7f5ac..1a943a6f3e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -36,12 +36,6 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 - val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_MANDATORY = 14 - val CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_OPTIONAL = 15 - - val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY = 22 - val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL = 23 - def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) def hasFeature(features: BinaryData, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit) 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 58aea52237..66af24105d 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 @@ -120,23 +120,13 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) - val remoteHasChannelRangeQueriesDeprecatedOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesDeprecatedMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_DEPRECATED_BIT_MANDATORY) - val remoteHasChannelRangeQueriesExtendedOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesExtendedMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY) log.info(s"peer has globalFeatures=${remoteInit.globalFeatures} localFeatures=${remoteInit.localFeatures}: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") if (Features.areSupported(remoteInit.localFeatures)) { d.origin_opt.foreach(origin => origin ! "connected") if (remoteHasInitialRoutingSync) { - if (remoteHasChannelRangeQueriesExtendedOptional || remoteHasChannelRangeQueriesExtendedMandatory) { - // if they support extended channel queries we do nothing, they will send us their filters - log.info("peer has set initial routing sync and supports extended channel range queries, we do nothing (they will send us a query)") - } else if (remoteHasChannelRangeQueriesDeprecatedOptional || remoteHasChannelRangeQueriesDeprecatedMandatory) { - // if they support extended channel queries we do nothing, they will send us their filters - log.info("peer has set initial routing sync and supports deprecated channel range queries, we do nothing (they will send us a query)") - } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries we do nothing, they will send us their filters log.info("peer has set initial routing sync and supports channel range queries, we do nothing (they will send us a query)") } else { @@ -145,15 +135,9 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } } - if (remoteHasChannelRangeQueriesExtendedOptional || remoteHasChannelRangeQueriesExtendedMandatory) { - // if they support extended channel queries, always ask for their filter - router ! SendChannelQueryWithChecksums(remoteNodeId, d.transport) - } else if (remoteHasChannelRangeQueriesDeprecatedOptional || remoteHasChannelRangeQueriesDeprecatedMandatory) { - // if they support proto channel queries, always ask for their filter - router ! SendChannelQueryDeprecated(remoteNodeId, d.transport) - } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter - router ! SendChannelQuery(remoteNodeId, d.transport) + router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = None) } // let's bring existing/requested channels online 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 0756b506b6..43ac2f5547 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 @@ -76,20 +76,15 @@ case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a Tempo case class LiftChannelExclusion(desc: ChannelDesc) // channel queries as specified in BOLT 1.0 -case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) - -// channel queries with one extra timestamp, used by eclair prototypes -// remove ASAP i.e as soon as mobile apps have been updated with the new queries below -case class SendChannelQueryDeprecated(remoteNodeId: PublicKey, to: ActorRef) - -// channel queries with 2 extra timestamps (one per chanel update) and a checksum, proposed for BOLT 1.1 -case class SendChannelQueryWithChecksums(remoteNodeId: PublicKey, to: ActorRef) +case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef, flags_opt: Option[Byte]) case object GetRoutingState case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) +case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Byte) + case class Sync(pending: List[RoutingMessage], total: Int) case class Data(nodes: Map[PublicKey, NodeAnnouncement], @@ -439,11 +434,11 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom .recover { case t => sender ! Status.Failure(t) } stay - case Event(SendChannelQuery(remoteNodeId, remote), d) => + case Event(SendChannelQuery(remoteNodeId, remote, flags_opt), d) => // ask for everything // we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't // have to worry about sending a new query_channel_range when another query is still in progress - val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue, optionExtendedQueryFlags_opt = flags_opt) log.info("sending query_channel_range={}", query) remote ! query @@ -458,30 +453,6 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) - case Event(SendChannelQueryDeprecated(remoteNodeId, remote), d) => - // ask for everything - val query = QueryChannelRangeDeprecated(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) - log.info("sending query_channel_range_proto={}", query) - remote ! query - // we also set a pass-all filter for now (we can update it later) - val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) - remote ! filter - // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and - // will start a new complete sync process - stay using d.copy(sync = d.sync - remoteNodeId) - - case Event(SendChannelQueryWithChecksums(remoteNodeId, remote), d) => - // ask for everything - val query = QueryChannelRangeWithChecksums(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) - log.info("sending query_channel_range_with_checksums={}", query) - remote ! query - // we also set a pass-all filter for now (we can update it later) - val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) - remote ! filter - // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and - // will start a new complete sync process - stay using d.copy(sync = d.sync - remoteNodeId) - // Warning: order matters here, this must be the first match for HasChainHash messages ! case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), d) if routingMessage.chainHash != nodeParams.chainHash => sender ! TransportHandler.ReadAck(routingMessage) @@ -537,108 +508,44 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom log.debug("received node announcement for nodeId={}", n.nodeId) stay using handle(n, sender, d) - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, optionExtendedQueryFlags_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) log.info("received {}", routingMessage) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) split(shortChannelIds) - .foreach(chunk => transport ! ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds))) + .foreach(chunk => + transport ! ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, + complete = 1, + shortChannelIds = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds), + optionExtendedQueryFlags_opt = optionExtendedQueryFlags_opt, + extendedInfo_opt = optionExtendedQueryFlags_opt map { + case ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS => ExtendedInfo(chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates))) + })) stay - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - log.info("received {}", routingMessage) - // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) - log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - split(shortChannelIds) - .foreach(chunk => transport ! ReplyChannelRangeDeprecated(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds.map(id => ShortChannelIdWithTimestamp(id, getTimestamp(d.channels, d.updates)(id)))))) - stay - - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - log.info("received {}", routingMessage) - // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) - log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) - split(shortChannelIds) - .foreach(chunk => transport ! ReplyChannelRangeWithChecksums(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, data = EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates))))) - stay - - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - val theirShortChannelIds: SortedSet[ShortChannelId] = SortedSet(data.array: _*) - val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) - val missing: SortedSet[ShortChannelId] = theirShortChannelIds -- ourShortChannelIds - log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={}", missing.size, data.encoding) - // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) - val replies = missing - .grouped(SHORTID_WINDOW) - .map(chunk => QueryShortChannelIds(chainHash, data = EncodedShortChannelIds(data.encoding, chunk.toList))) - .toList - val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) - // we only send a rely right away if there were no pending requests - replynow_opt.foreach(transport ! _) - context.system.eventStream.publish(syncProgress(sync1)) - stay using d.copy(sync = sync1) - - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeDeprecated(chainHash, _, _, _, data)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - val missing = data.array - .filter { channelInfo => - // we request unknown channels - !d.channels.contains(channelInfo.shortChannelId) || - // and known channels for which our timestamp is older than theirs - Router.getTimestamp(d.channels, d.updates)(channelInfo.shortChannelId) < channelInfo.timestamp - } - .map(_.shortChannelId) - log.info("received reply_channel_range_deprecated, we're missing {} channel announcements/updates, format={}", missing.size, data.encoding) - // TODO: simplification! we always request all data (this is deprecated anyway) - val flag = (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte - // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) - val replies = missing - .grouped(SHORTID_WINDOW) - .map(chunk => QueryShortChannelIdsDeprecated(chainHash, flag, data = EncodedShortChannelIds(data.encoding, chunk))) - .toList - val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) - // we only send a rely right away if there were no pending requests - replynow_opt.foreach(transport ! _) - context.system.eventStream.publish(syncProgress(sync1)) - stay using d.copy(sync = sync1) - - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeWithChecksums(chainHash, _, _, _, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, data, optionExtendedQueryFlags_opt, extendedInfo_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) val shortChannelIdAndFlags = data.array - .map { theirInfo => - var flag = 0 - if (d.channels.contains(theirInfo.shortChannelId)) { - val ourInfo = Router.getChannelDigestInfo(d.channels, d.updates)(theirInfo.shortChannelId) - // we request their channel_update if all those conditions are met: - // - it is more recent than ours - // - it is different from ours, or it is the same but ours is about to be stale - // - it is not stale itself - if (ourInfo.timestamp1 < theirInfo.timestamp1 && (ourInfo.checksum1 != theirInfo.checksum1 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp1)) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 - if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | FlagTypes.INCLUDE_CHANNEL_UPDATE_2 - } else { - // we don't know this channel: we request everything - flag = flag | FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2 - } - ShortChannelIdAndFlag(theirInfo.shortChannelId, flag.toByte) - } - .filter(_.flag != 0) + .zipWithIndex + .map { case (shortChannelId: ShortChannelId, idx) => ShortChannelIdAndFlag(shortChannelId, computeFlag(d.channels, d.updates)(shortChannelId, extendedInfo_opt.map(_.array(idx)))) } + .filter(_.flag != 0) val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) { case ((c, u), ShortChannelIdAndFlag(_, flag)) => - val c1 = c + (if (FlagTypes.includeAnnouncement(flag)) 1 else 0) - val u1 = u + (if (FlagTypes.includeUpdate1(flag)) 1 else 0) + (if (FlagTypes.includeUpdate2(flag)) 1 else 0) + val c1 = c + (if (QueryFlagTypes.includeAnnouncement(flag)) 1 else 0) + val u1 = u + (if (QueryFlagTypes.includeUpdate1(flag)) 1 else 0) + (if (QueryFlagTypes.includeUpdate2(flag)) 1 else 0) (c1, u1) } - log.info("received reply_channel_range_with_checksums with {} channels, we're missing {} channel announcements and {} updates, format={}", data.array.size, channelCount, updatesCount, data.encoding) + log.info("received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", data.array.size, channelCount, updatesCount, data.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) val replies = shortChannelIdAndFlags .grouped(SHORTID_WINDOW) - .map(chunk => QueryShortChannelIdsWithFlags(chainHash, data = EncodedShortChannelIdsAndFlag(data.encoding, chunk))) + .map(chunk => QueryShortChannelIds(chainHash, + shortChannelIds = EncodedShortChannelIds(data.encoding, chunk.map(_.shortChannelId)), + queryFlags_opt = optionExtendedQueryFlags_opt map { + case _ => EncodedQueryFlags(data.encoding, chunk.map(_.flag)) + })) .toList val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) // we only send a rely right away if there were no pending requests @@ -646,55 +553,35 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom context.system.eventStream.publish(syncProgress(sync1)) stay using d.copy(sync = sync1) - // standard query message: a list of channel ids - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, data)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, shortChannelIds, queryFlags_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (channelCount, updatesCount) = handleChannelQuery(d, transport, data.array, - id = (id: ShortChannelId) => id, - sendChannel = (_: ShortChannelId) => true, - sendUpdate1 = (_: ShortChannelId) => true, - sendUpdate2 = (_: ShortChannelId) => true) - log.info("received query_short_channel_ids with {} items, sent back {} channels and {} updates", data.array.size, channelCount, updatesCount) + val (channelCount, updatesCount) = shortChannelIds.array + .zipWithIndex + .foldLeft((0, 0)) { + case ((c, u), (shortChannelId, idx)) => + var c1 = c + var u1 = u + val flag = queryFlags_opt.map(_.array(idx)).getOrElse((QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte) + d.channels.get(shortChannelId) match { + case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) + case Some(ca) => + if (QueryFlagTypes.includeAnnouncement(flag)) { + transport ! ca + c1 = c1 + 1 + } + if (QueryFlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } + if (QueryFlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } + } + (c1, u1) + } + log.info("received query_short_channel_ids with {} items, sent back {} channels and {} updates", shortChannelIds.array.size, channelCount, updatesCount) transport ! ReplyShortChannelIdsEnd(chainHash, 1) stay - // extended query message: a flag and a list of channel ids - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsDeprecated(chainHash, flag, data)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - // TODO: for backward compatibility we always return all data - val (channelCount, updatesCount) = handleChannelQuery(d, transport, data.array, - id = (id: ShortChannelId) => id, - sendChannel = (_: ShortChannelId) => true, - sendUpdate1 = (_: ShortChannelId) => true, - sendUpdate2 = (_: ShortChannelId) => true) - log.info("received query_short_channel_ids_deprecated with {} items, flag={} (ignored!!), sent back {} channels and {} updates", data.array.size, flag, channelCount, updatesCount) - transport ! ReplyShortChannelIdsEndDeprecated(chainHash, 1) - stay - - // new extended query message: a list of [channel id + flag] - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsWithFlags(chainHash, data)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - val (channelCount, updatesCount) = handleChannelQuery(d, transport, data.array, - id = (item: ShortChannelIdAndFlag) => item.shortChannelId, - sendChannel = (item: ShortChannelIdAndFlag) => FlagTypes.includeAnnouncement(item.flag), - sendUpdate1 = (item: ShortChannelIdAndFlag) => FlagTypes.includeUpdate1(item.flag), - sendUpdate2 = (item: ShortChannelIdAndFlag) => FlagTypes.includeUpdate2(item.flag)) - log.info("received query_short_channel_ids_ex with {} items, sent back {} channels and {} updates", data.array.size, channelCount, updatesCount) - transport ! ReplyShortChannelIdsWithFlagsEnd(chainHash, 1) - stay - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsEnd), d) => sender ! TransportHandler.ReadAck(routingMessage) stay using handleSyncEnd(d, remoteNodeId, transport) - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsEndDeprecated), d) => - sender ! TransportHandler.ReadAck(routingMessage) - stay using handleSyncEnd(d, remoteNodeId, transport) - - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsWithFlagsEnd), d) => - sender ! TransportHandler.ReadAck(routingMessage) - stay using handleSyncEnd(d, remoteNodeId, transport) - } initialize() @@ -824,7 +711,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // when we're sending updates to ourselves (transport_opt, remoteNodeId_opt) match { case (Some(transport), Some(remoteNodeId)) => - val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(u.shortChannelId))) + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(u.shortChannelId)), queryFlags_opt = None) d.sync.get(remoteNodeId) match { case Some(sync) => // we already have a pending request to that node, let's add this channel to the list and we'll get it later @@ -867,28 +754,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom d.copy(sync = sync1) } - def handleChannelQuery[T](d: Data, transport: ActorRef, items: Iterable[T], id: T => ShortChannelId, sendChannel: T => Boolean, sendUpdate1: T => Boolean, sendUpdate2: T => Boolean): (Int, Int) = { - items.foldLeft((0, 0)) { - case ((c, u), item) => - var c1 = c - var u1 = u - val shortChannelId = id(item) - d.channels.get(shortChannelId) match { - case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) - case Some(ca) => - if (sendChannel(item)) { - transport ! ca - c1 = c1 + 1 - } - if (sendUpdate1(item)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } - if (sendUpdate2(item)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } - } - (c1, u1) - } - } - override def mdc(currentMessage: Any): MDC = currentMessage match { - case SendChannelQuery(remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) + case SendChannelQuery(remoteNodeId, _, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) case PeerRoutingMessage(_, remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) case _ => akka.event.Logging.emptyMDC } @@ -972,6 +839,24 @@ object Router { height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } + def computeFlag(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId, theirInfo_opt: Option[TimestampsAndChecksums]): Byte = { + var flag = 0 + if (channels.contains(shortChannelId) || theirInfo_opt.isEmpty) { + val ourInfo = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + val theirInfo = theirInfo_opt.get + // we request their channel_update if all those conditions are met: + // - it is more recent than ours + // - it is different from ours, or it is the same but ours is about to be stale + // - it is not stale itself + if (ourInfo.timestamp1 < theirInfo.timestamp1 && (ourInfo.checksum1 != theirInfo.checksum1 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp1)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 + if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + } else { + // we don't know this channel: we request everything + QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + } + flag.toByte + } + /** * Returns overall progress on synchronization * @@ -1023,7 +908,7 @@ object Router { timestamp } - def getChannelDigestInfo(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId): ShortChannelIdWithChecksums = { + def getChannelDigestInfo(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId): TimestampsAndChecksums = { val c = channels(shortChannelId) val u1_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) val u2_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) @@ -1031,7 +916,7 @@ object Router { val timestamp2 = u2_opt.map(_.timestamp).getOrElse(0L) val checksum1 = u1_opt.map(getChecksum).getOrElse(0L) val checksum2 = u2_opt.map(getChecksum).getOrElse(0L) - ShortChannelIdWithChecksums(shortChannelId, timestamp1, timestamp2, checksum1, checksum2) + TimestampsAndChecksums(timestamp1, timestamp2, checksum1, checksum2) } def getChecksum(u: ChannelUpdate): Long = { 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 479a14b944..6325b47e15 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 @@ -316,9 +316,15 @@ object LightningMessageCodecs { .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIds(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) + val encodedQueryFlagsCodec: Codec[EncodedQueryFlags] = + discriminated[EncodedQueryFlags].by(byte) + .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedQueryFlags(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(byte)).as[EncodedQueryFlags]) + .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedQueryFlags(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(byte))).as[EncodedQueryFlags]) + val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( ("chainHash" | binarydata(32)) :: - ("data" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) + ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: + ("queryFlags_opt" | optional(bitsRemaining, variableSizeBytes(uint16, encodedQueryFlagsCodec))) ).as[QueryShortChannelIds] val replyShortChanelIdsEndCodec: Codec[ReplyShortChannelIdsEnd] = ( @@ -329,15 +335,27 @@ object LightningMessageCodecs { val queryChannelRangeCodec: Codec[QueryChannelRange] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) + ("numberOfBlocks" | uint32) :: + ("optionExtendedQueryFlags" | optional(bitsRemaining, byte)) ).as[QueryChannelRange] + val timestampsAndChecksumsCodec: Codec[TimestampsAndChecksums] = ( + ("timestamp1" | uint32) :: + ("timestamp2" | uint32) :: + ("checksum1" | uint32) :: + ("checksum2" | uint32) + ).as[TimestampsAndChecksums] + + val extendedInfoCodec: Codec[ExtendedInfo] = list(timestampsAndChecksumsCodec).as[ExtendedInfo] + val replyChannelRangeCodec: Codec[ReplyChannelRange] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: ("complete" | byte) :: - ("data" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) + ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: + ("optionExtendedQueryFlags_opt" | optional(bitsRemaining, byte)) :: + ("extendedInfo_opt" | optional(bitsRemaining, variableSizeBytes(uint16, extendedInfoCodec))) ).as[ReplyChannelRange] val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( @@ -346,88 +364,6 @@ object LightningMessageCodecs { ("timestampRange" | uint32) ).as[GossipTimestampFilter] - val queryShortChannelIdsDeprecatedCodec: Codec[QueryShortChannelIdsDeprecated] = ( - ("chainHash" | binarydata(32)) :: - ("flag" | byte) :: - ("data" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) - ).as[QueryShortChannelIdsDeprecated] - - val replyShortChanelIdsEndDeprecatedCodec: Codec[ReplyShortChannelIdsEndDeprecated] = ( - ("chainHash" | binarydata(32)) :: - ("complete" | byte) - ).as[ReplyShortChannelIdsEndDeprecated] - - val queryChannelRangeDeprecatedCodec: Codec[QueryChannelRangeDeprecated] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) - ).as[QueryChannelRangeDeprecated] - - val shortChannelIdWithTimestampCodec: Codec[ShortChannelIdWithTimestamp] = ( - ("shortChannelId" | shortchannelid) :: - ("timestamp" | uint32) - ).as[ShortChannelIdWithTimestamp] - - val channelRangeWithTimestampDataCodec: Codec[EncodedShortChannelIdsWithTimestamp] = - discriminated[EncodedShortChannelIdsWithTimestamp].by(byte) - .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortChannelIdWithTimestampCodec)).as[EncodedShortChannelIdsWithTimestamp]) - .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIdsWithTimestamp(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortChannelIdWithTimestampCodec))).as[EncodedShortChannelIdsWithTimestamp]) - - val replyChannelRangeDeprecatedCodec: Codec[ReplyChannelRangeDeprecated] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) :: - ("complete" | byte) :: - ("data" | variableSizeBytes(uint16, channelRangeWithTimestampDataCodec)) - ).as[ReplyChannelRangeDeprecated] - - val shortChannelIdAndFlagCodec: Codec[ShortChannelIdAndFlag] = ( - ("shortChannelId" | shortchannelid) :: - ("flag" | byte) - ).as[ShortChannelIdAndFlag] - - val encodedShortChannelIdsAndFlagCodec: Codec[EncodedShortChannelIdsAndFlag] = - discriminated[EncodedShortChannelIdsAndFlag].by(byte) - .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIdsAndFlag(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortChannelIdAndFlagCodec)).as[EncodedShortChannelIdsAndFlag]) - .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIdsAndFlag(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortChannelIdAndFlagCodec))).as[EncodedShortChannelIdsAndFlag]) - - val queryShortChannelIdsWithFlagsCodec: Codec[QueryShortChannelIdsWithFlags] = ( - ("chainHash" | binarydata(32)) :: - ("data" | variableSizeBytes(uint16, encodedShortChannelIdsAndFlagCodec)) - ).as[QueryShortChannelIdsWithFlags] - - val replyShortChannelIdsWithFlagsEndCodec: Codec[ReplyShortChannelIdsWithFlagsEnd] = ( - ("chainHash" | binarydata(32)) :: - ("complete" | byte) - ).as[ReplyShortChannelIdsWithFlagsEnd] - - val queryChannelRangeWithChecksumsCodec: Codec[QueryChannelRangeWithChecksums] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) - ).as[QueryChannelRangeWithChecksums] - - val shortChannelIdWithChecksumsCodec: Codec[ShortChannelIdWithChecksums] = ( - ("shortChannelId" | shortchannelid) :: - ("timestamp1" | uint32) :: - ("timestamp2" | uint32) :: - ("checksum1" | uint32) :: - ("checksum2" | uint32) - ).as[ShortChannelIdWithChecksums] - - val channelRangeWithChecksumsDataCodec: Codec[EncodedShortChannelIdsWithChecksums] = - discriminated[EncodedShortChannelIdsWithChecksums].by(byte) - .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortChannelIdWithChecksumsCodec)).as[EncodedShortChannelIdsWithChecksums]) - .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIdsWithChecksums(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortChannelIdWithChecksumsCodec))).as[EncodedShortChannelIdsWithChecksums]) - - val replyChannelRangeWithChecksumsCodec: Codec[ReplyChannelRangeWithChecksums] = ( - ("chainHash" | binarydata(32)) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) :: - ("complete" | byte) :: - ("data" | variableSizeBytes(uint16, channelRangeWithChecksumsDataCodec)) - ).as[ReplyChannelRangeWithChecksums] - val lightningMessageCodec = discriminated[LightningMessage].by(uint16) .typecase(16, initCodec) .typecase(17, errorCodec) @@ -457,14 +393,6 @@ object LightningMessageCodecs { .typecase(263, queryChannelRangeCodec) .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) - .typecase(1001, queryShortChannelIdsDeprecatedCodec) - .typecase(1002, replyShortChanelIdsEndDeprecatedCodec) - .typecase(1003, queryChannelRangeDeprecatedCodec) - .typecase(1004, replyChannelRangeDeprecatedCodec) - .typecase(1011, queryShortChannelIdsWithFlagsCodec) - .typecase(1012, replyShortChannelIdsWithFlagsEndCodec) - .typecase(1013, queryChannelRangeWithChecksumsCodec) - .typecase(1014, replyChannelRangeWithChecksumsCodec) /** * A codec that caches serialized routing messages 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 d2d455b685..65744cda98 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 @@ -229,97 +229,55 @@ case object EncodingTypes { val COMPRESSED_ZLIB: Byte = 1 } +case object QueryFlagTypes { + val INCLUDE_CHANNEL_UPDATE_1: Byte = 1 + val INCLUDE_CHANNEL_UPDATE_2: Byte = 2 + val INCLUDE_ANNOUNCEMENT: Byte = 4 + + def includeAnnouncement(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_ANNOUNCEMENT) != 0 + + def includeUpdate1(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1) != 0 + + def includeUpdate2(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2) != 0 +} + case class EncodedShortChannelIds(encoding: Byte, array: List[ShortChannelId]) +case class EncodedQueryFlags(encoding: Byte, + array: List[Byte]) + case class QueryShortChannelIds(chainHash: BinaryData, - data: EncodedShortChannelIds) extends RoutingMessage with HasChainHash + shortChannelIds: EncodedShortChannelIds, + queryFlags_opt: Option[EncodedQueryFlags]) extends RoutingMessage with HasChainHash case class ReplyShortChannelIdsEnd(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash +case object ExtendedQueryFlags { + val TIMESTAMPS_AND_CHECKSUMS: Byte = 1 +} + case class QueryChannelRange(chainHash: BinaryData, firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash + numberOfBlocks: Long, + optionExtendedQueryFlags_opt: Option[Byte]) extends RoutingMessage with HasChainHash case class ReplyChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, - data: EncodedShortChannelIds) extends RoutingMessage with HasChainHash + shortChannelIds: EncodedShortChannelIds, + optionExtendedQueryFlags_opt: Option[Byte], + extendedInfo_opt: Option[ExtendedInfo]) extends RoutingMessage with HasChainHash case class GossipTimestampFilter(chainHash: BinaryData, firstTimestamp: Long, timestampRange: Long) extends RoutingMessage with HasChainHash -// prototype queries, used by eclair only, to be removed asap - -case object FlagTypes { - val INCLUDE_CHANNEL_UPDATE_1: Byte = 1 - val INCLUDE_CHANNEL_UPDATE_2: Byte = 2 - val INCLUDE_ANNOUNCEMENT: Byte = 4 - - def includeAnnouncement(flag: Byte) = (flag & FlagTypes.INCLUDE_ANNOUNCEMENT) != 0 - - def includeUpdate1(flag: Byte) = (flag & FlagTypes.INCLUDE_CHANNEL_UPDATE_1) != 0 - - def includeUpdate2(flag: Byte) = (flag & FlagTypes.INCLUDE_CHANNEL_UPDATE_2) != 0 -} - -case class QueryShortChannelIdsDeprecated(chainHash: BinaryData, - flag: Byte, - data: EncodedShortChannelIds) extends RoutingMessage with HasChainHash - -case class ReplyShortChannelIdsEndDeprecated(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash - -case class QueryChannelRangeDeprecated(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash - -case class ShortChannelIdWithTimestamp(shortChannelId: ShortChannelId, - timestamp: Long) - -case class EncodedShortChannelIdsWithTimestamp(encoding: Byte, - array: List[ShortChannelIdWithTimestamp]) - -case class ReplyChannelRangeDeprecated(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: EncodedShortChannelIdsWithTimestamp) extends RoutingMessage with HasChainHash - -// proposal for BOLT 1.1 channel queries - -case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, - flag: Byte) - -case class EncodedShortChannelIdsAndFlag(encoding: Byte, - array: List[ShortChannelIdAndFlag]) - -case class QueryShortChannelIdsWithFlags(chainHash: BinaryData, - data: EncodedShortChannelIdsAndFlag) extends RoutingMessage with HasChainHash - -case class ReplyShortChannelIdsWithFlagsEnd(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash - -// 2nd proposal with checksums - -case class QueryChannelRangeWithChecksums(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash - -case class ShortChannelIdWithChecksums(shortChannelId: ShortChannelId, - timestamp1: Long, - timestamp2: Long, - checksum1: Long, - checksum2: Long) - -case class EncodedShortChannelIdsWithChecksums(encoding: Byte, - array: List[ShortChannelIdWithChecksums]) +case class TimestampsAndChecksums(timestamp1: Long, + checksum1: Long, + timestamp2: Long, + checksum2: Long) -case class ReplyChannelRangeWithChecksums(chainHash: BinaryData, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: EncodedShortChannelIdsWithChecksums) extends RoutingMessage with HasChainHash \ No newline at end of file +case class ExtendedInfo(array: List[TimestampsAndChecksums]) \ No newline at end of file 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 0d6a4431ad..b0253fa655 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 @@ -150,7 +150,7 @@ class PeerSpec extends TestkitBaseClass { val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) - val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42000)))) + val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42000))), queryFlags_opt = None) // make sure that routing messages go through for (ann <- channels ++ updates) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 4cc5b4e4d3..312f1ca635 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Block import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.{ReplyChannelRange, ReplyChannelRangeDeprecated} +import fr.acinq.eclair.wire.ReplyChannelRange import org.scalatest.FunSuite import scala.collection.immutable.SortedMap diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala deleted file mode 100644 index edde670551..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesWithTimestampsSpec.scala +++ /dev/null @@ -1,33 +0,0 @@ -package fr.acinq.eclair.router - -import fr.acinq.bitcoin.Block -import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.ReplyChannelRangeDeprecated -import org.scalatest.FunSuite - -import scala.collection.immutable.SortedMap -import scala.util.Random - -class ChannelRangeQueriesWithTimestampsSpec extends FunSuite { - val random = new Random() - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds - val timestamps = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap - - /*test("create `reply_channel_range_ex` messages (uncompressed format)") { - val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamps(id) == decoded(id)) - } - - test("create `reply_channel_range_ex` messages (zlib format)") { - val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamps(id) == decoded(id)) - }*/ -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 5a2631d392..581cf83fb7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -261,6 +261,6 @@ class RouterSpec extends BaseRouterSpec { val transport = TestProbe() probe.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, update1)) val query = transport.expectMsgType[QueryShortChannelIds] - assert(query.data.array == List(channelId)) + assert(query.shortChannelIds.array == List(channelId)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala deleted file mode 100644 index a368e0f443..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncDeprecatedSpec.scala +++ /dev/null @@ -1,82 +0,0 @@ -package fr.acinq.eclair.router - -import akka.actor.ActorSystem -import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.eclair._ -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.wire._ -import org.scalatest.FunSuiteLike - -import scala.collection.immutable.TreeMap -import scala.concurrent.duration._ - - -class RoutingSyncDeprecatedSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { - import RoutingSyncSpec.makeFakeRoutingInfo - - test("handle channel range extended queries") { - val params = TestConstants.Alice.nodeParams - val router = TestFSMRef(new Router(params, TestProbe().ref)) - val transport = TestProbe() - val sender = TestProbe() - sender.ignoreMsg { case _: TransportHandler.ReadAck => true } - val remoteNodeId = TestConstants.Bob.nodeParams.nodeId - - // ask router to send a channel range query - sender.send(router, SendChannelQueryDeprecated(remoteNodeId, sender.ref)) - val QueryChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeDeprecated] - sender.expectMsgType[GossipTimestampFilter] - - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => - val desc = Router.getDesc(u, initChannels(u.shortChannelId)) - (desc) -> u - }.toMap - - // split our anwser in 3 blocks - val block1 = ReplyChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList.map(id => ShortChannelIdWithTimestamp(id, Router.getTimestamp(initChannels, initChannelUpdates)(id))))) - val block2 = ReplyChannelRangeDeprecated(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList.map(id => ShortChannelIdWithTimestamp(id, Router.getTimestamp(initChannels, initChannelUpdates)(id))))) - - // send first block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) - // router should ask for our first block of ids - assert(transport.expectMsgType[QueryShortChannelIdsDeprecated] === QueryShortChannelIdsDeprecated(chainHash, (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte, EncodedShortChannelIds(block1.data.encoding, block1.data.array.map(_.shortChannelId)))) - - // send second block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block2)) - - // router should not ask for more ids, it already has a pending query ! - sender.expectNoMsg(1 second) - - // send the first 50 items - block1.data.array.map(_.shortChannelId).take(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - - sender.expectNoMsg(1 second) - - // send the last 50 items - block1.data.array.map(_.shortChannelId).drop(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - - // during that time, router should not have asked for more ids, it already has a pending query ! - transport.expectNoMsg(200 millis) - - // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEndDeprecated(chainHash, 1.toByte))) - - // router should ask for our second block of ids - assert(transport.expectMsgType[QueryShortChannelIdsDeprecated] === QueryShortChannelIdsDeprecated(chainHash, (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte, EncodedShortChannelIds(block1.data.encoding, block2.data.array.map(_.shortChannelId)))) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 6f83848d8d..c7f93274ed 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -48,18 +48,18 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val remoteNodeId = TestConstants.Bob.nodeParams.nodeId // ask router to send a channel range query - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) - val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, flags_opt = None)) + val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] // split our answer in 3 blocks - val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList)) - val block2 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList)) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) + val block2 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList), None, None) // send first block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) // router should ask for our first block of ids - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.data)) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.shortChannelIds, None)) // send second block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block2)) @@ -68,7 +68,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.expectNoMsg(1 second) // send the first 50 items - block1.data.array.take(50).foreach(id => { + block1.shortChannelIds.array.take(50).foreach(id => { val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) @@ -78,7 +78,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.expectNoMsg(1 second) // send the last 50 items - block1.data.array.drop(50).foreach(id => { + block1.shortChannelIds.array.drop(50).foreach(id => { val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) @@ -92,7 +92,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) // router should ask for our second block of ids - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block2.data)) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block2.shortChannelIds, None)) } test("reset sync state on reconnection") { @@ -104,23 +104,23 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val remoteNodeId = TestConstants.Bob.nodeParams.nodeId // ask router to send a channel range query - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) - val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, None)) + val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList)) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) // send first block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) // router should ask for our first block of ids - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.data)) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.shortChannelIds, None)) // router should think that it is missing 100 channels, in one request val Some(sync) = router.stateData.sync.get(remoteNodeId) assert(sync.total == 1) // simulate a re-connection - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, None)) sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] assert(router.stateData.sync.get(remoteNodeId).isEmpty) @@ -128,7 +128,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { test("sync progress") { - def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42)))) + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42))), None) val nodeidA = randomKey.publicKey val nodeidB = randomKey.publicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala index f4e682e55c..8dec898242 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala @@ -52,22 +52,26 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun val remoteNodeId = TestConstants.Bob.nodeParams.nodeId // ask router to send a channel range query - sender.send(router, SendChannelQueryWithChecksums(remoteNodeId, sender.ref)) - val QueryChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeWithChecksums] + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS))) + val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, Some(optionExtendedQueryFlags)) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] // send back all our ids and timestamps - val block = ReplyChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, initChannelUpdates)))) + val block = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, + shortChannelIds = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList), + optionExtendedQueryFlags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), + extendedInfo_opt = Some(ExtendedInfo(shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, initChannelUpdates)))) + ) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block)) // router should ask for our first block of ids - val shortChannelIdAndFlags = block.data.array.map(info => ShortChannelIdAndFlag(info.shortChannelId, (FlagTypes.INCLUDE_ANNOUNCEMENT | FlagTypes.INCLUDE_CHANNEL_UPDATE_1 | FlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte)) + val shortChannelIdAndFlags = block.shortChannelIds.array.map(shortChannelId => ShortChannelIdAndFlag(shortChannelId, (QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte)) val shortChannelIdAndFlags1 = shortChannelIdAndFlags.take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags1))) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags1.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags1.map(_.flag))))) // send the first 50 items - shortChannelIdAndFlags1.take(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) + shortChannelIdAndFlags1.take(50).foreach(info => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(info.shortChannelId) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) @@ -75,8 +79,8 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun sender.expectNoMsg(1 second) // send the last 50 items - shortChannelIdAndFlags1.drop(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) + shortChannelIdAndFlags1.drop(50).foreach(info => { + val (ca, cu1, cu2, _, _) = fakeRoutingInfo(info.shortChannelId) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) @@ -84,11 +88,11 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun sender.expectNoMsg(1 second) // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) // router should ask for our second block of ids val shortChannelIdAndFlags2 = shortChannelIdAndFlags.drop(Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags2))) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags2.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags2.map(_.flag))))) // send block #2 shortChannelIdAndFlags2.foreach(id => { @@ -97,11 +101,11 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) // router should ask for our third block of ids val shortChannelIdAndFlags3 = shortChannelIdAndFlags.drop(2 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags3))) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags3.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags3.map(_.flag))))) // send block #3 shortChannelIdAndFlags3.foreach(id => { @@ -110,11 +114,11 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) // router should ask for our fourth block of ids val shortChannelIdAndFlags4 = shortChannelIdAndFlags.drop(3 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags4))) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags4.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags4.map(_.flag))))) // send block #4 shortChannelIdAndFlags4.foreach(id => { @@ -123,13 +127,12 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) }) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsWithFlagsEnd(chainHash, 1.toByte))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) awaitCond({ router.stateData.channels == initChannels }, max = 30 seconds, interval = 500 millis) - val updatedIds = shortChannelIds.drop(100).take(50) val recentChannelUpdates = updatedIds.foldLeft(initChannelUpdates) { case (updates, id) => @@ -141,17 +144,20 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun updates.updated(desc, newUpdate) } // ask router to send a channel range query - sender.send(router, SendChannelQueryWithChecksums(remoteNodeId, sender.ref)) - sender.expectMsgType[QueryChannelRangeWithChecksums] + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS))) + assert(sender.expectMsgType[QueryChannelRange].optionExtendedQueryFlags_opt.isDefined) sender.expectMsgType[GossipTimestampFilter] // send back all our ids and timestamps - val block1 = ReplyChannelRangeWithChecksums(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIdsWithChecksums(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, recentChannelUpdates)))) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, + EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList), + Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), + Some(ExtendedInfo(shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, recentChannelUpdates))))) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) // router should ask for our new channel updates - val shortChannelIdAndFlags5 = block1.data.array.map(info => ShortChannelIdAndFlag(info.shortChannelId, FlagTypes.INCLUDE_CHANNEL_UPDATE_1)).filter(info => updatedIds.contains(info.shortChannelId)) - assert(transport.expectMsgType[QueryShortChannelIdsWithFlags] === QueryShortChannelIdsWithFlags(chainHash, EncodedShortChannelIdsAndFlag(block.data.encoding, shortChannelIdAndFlags5))) + val shortChannelIdAndFlags5 = block1.shortChannelIds.array.map(shortChannelId => ShortChannelIdAndFlag(shortChannelId, QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1)).filter(info => updatedIds.contains(info.shortChannelId)) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags5.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags5.map(_.flag))))) } } 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 35c430589e..3f22f2017e 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 @@ -241,9 +241,9 @@ class LightningMessageCodecsSpec extends FunSuite { val channel_update = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, 4, 5, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes(32), ShortChannelId(42), randomSignature, randomSignature) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676)))) - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676)))) + val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None) + val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) + val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(1, 1, 1, 1), TimestampsAndChecksums(2, 2, 2, 2), TimestampsAndChecksums(3, 3, 3, 3))))) val ping = Ping(100, BinaryData("01" * 10)) val pong = Pong(BinaryData("01" * 10)) val channel_reestablish = ChannelReestablish(randomBytes(32), 242842L, 42L) @@ -319,78 +319,6 @@ class LightningMessageCodecsSpec extends FunSuite { val bin2 = BinaryData(LightningMessageCodecs.lightningMessageCodec.encode(update).require.toByteArray) assert(bin === bin2) } - - test("nonreg tests") { - - val channels = SortedMap( - ShortChannelId(0xaa) -> (0x1000L, 0x1001L), - ShortChannelId(0xbb) -> (0x2000L, 0x2001L), - ShortChannelId(0xcc) -> (0x3000L, 0x3001L) - ) - - val chainHash = Block.RegtestGenesisBlock.hash - val firstBlockIn = 0xa0 - val numBlockIn = 0xff - - // val encodeManualOriginal = ShortChannelIdsBlock - // .encode(firstBlockIn, numBlockIn, channels.keySet, UNCOMPRESSED_FORMAT) - // .head - // .shortChannelIds - // val reply_channel_range_original_uncompressed = ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, encodeManualOriginal) - // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_original_uncompressed).require.toHex) - // - // val encodeManualOriginalCompressed = ShortChannelIdsBlock - // .encode(firstBlockIn, numBlockIn, channels.keySet, ZLIB_FORMAT) - // .head - // .shortChannelIds - // val reply_channel_range_original_compressed = ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, encodeManualOriginalCompressed) - // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_original_compressed).require.toHex) - // - // val encodeManualDeprecated = ShortChannelIdAndTimestampBlock - // .encode(firstBlockIn, numBlockIn, channels.keySet, id => channels(id)._1, UNCOMPRESSED_FORMAT) - // .head - // .shortChannelIdAndTimestamps - // val reply_channel_range_deprecated_uncompressed = ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, encodeManualDeprecated) - // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_deprecated_uncompressed).require.toHex) - // - // val encodeManualDeprecatedCompressed = ShortChannelIdAndTimestampBlock - // .encode(firstBlockIn, numBlockIn, channels.keySet, id => channels(id)._1, ZLIB_FORMAT) - // .head - // .shortChannelIdAndTimestamps - // val reply_channel_range_deprecated_compressed = ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, encodeManualDeprecatedCompressed) - // println(logToStdOut(lightningMessageCodec).encode(reply_channel_range_deprecated_compressed).require.toHex) - - // encoded ReplyChannelRange(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,0000000000000000aa00000000000000bb00000000000000cc) to Successful(BitVector(560 bits, #448413046)) - // 010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100190000000000000000aa00000000000000bb00000000000000cc - // encoded ReplyChannelRange(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,01789c6360008355108a6137943e030012c10232) to Successful(BitVector(520 bits, #-663753163)) - // 010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001401789c6360008355108a6137943e030012c10232 - // encoded ReplyChannelRangeDeprecated(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,0000000000000000aa0000100000000000000000bb0000200000000000000000cc00003000) to Successful(BitVector(656 bits, #1135811878)) - // 03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100250000000000000000aa0000100000000000000000bb0000200000000000000000cc00003000 - // encoded ReplyChannelRangeDeprecated(06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f,160,255,1,01789c63600083550c0c020c50b09b814101c63ec3c060c00000278d0292) to Successful(BitVector(600 bits, #-635546169)) - // 03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001e01789c63600083550c0c020c50b09b814101c63ec3c060c00000278d0292 - - val encodedManualOriginalBin = BinaryData("010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100190000000000000000aa00000000000000bb00000000000000cc") - val encodedManualOriginalCompressedBin = BinaryData("010806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001401789c6360008355108a6137943e030012c10232") - val encodedManualDeprecatedBin = BinaryData("03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff0100250000000000000000aa0000100000000000000000bb0000200000000000000000cc00003000") - val encodedManualDeprecatedCompressedBin = BinaryData("03ec06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f000000a0000000ff01001e01789c63600083550c0c020c50b09b814101c63ec3c060c00000278d0292") - - val expected = Map( - encodedManualOriginalBin -> ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, channels.keySet.toList)), - encodedManualOriginalCompressedBin -> ReplyChannelRange(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIds(EncodingTypes.COMPRESSED_ZLIB, channels.keySet.toList)), - encodedManualDeprecatedBin -> ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.UNCOMPRESSED, channels.toList.map(i => ShortChannelIdWithTimestamp(i._1, i._2._1)))), - encodedManualDeprecatedCompressedBin -> ReplyChannelRangeDeprecated(chainHash, firstBlockIn, numBlockIn, 1, EncodedShortChannelIdsWithTimestamp(EncodingTypes.COMPRESSED_ZLIB, channels.toList.map(i => ShortChannelIdWithTimestamp(i._1, i._2._1)))) - ) - - for ((bin, obj) <- expected) { - val decoded = lightningMessageCodec.decode(BitVector.apply(bin.data)).require.value - assert(decoded === obj) - val encoded = lightningMessageCodec.encode(decoded).require - assert(BinaryData(encoded.toByteArray) == bin) - } - - } - - } object LightningMessageCodecsSpec { From deb0708d72dfacd87a120e3eff4865e8373bcbda Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 11 Mar 2019 12:15:57 +0100 Subject: [PATCH 34/86] fixed tests --- .../scala/fr/acinq/eclair/router/Router.scala | 82 ++++++++++--------- .../eclair/wire/LightningMessageCodecs.scala | 8 +- .../eclair/wire/LightningMessageTypes.scala | 11 ++- 3 files changed, 56 insertions(+), 45 deletions(-) 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 43ac2f5547..d6af5f3f95 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 @@ -76,7 +76,7 @@ case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a Tempo case class LiftChannelExclusion(desc: ChannelDesc) // channel queries as specified in BOLT 1.0 -case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef, flags_opt: Option[Byte]) +case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef, flags_opt: Option[ExtendedQueryFlags]) case object GetRoutingState case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) @@ -525,26 +525,26 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom })) stay - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, data, optionExtendedQueryFlags_opt, extendedInfo_opt)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, shortChannelIds, optionExtendedQueryFlags_opt, extendedInfo_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val shortChannelIdAndFlags = data.array - .zipWithIndex - .map { case (shortChannelId: ShortChannelId, idx) => ShortChannelIdAndFlag(shortChannelId, computeFlag(d.channels, d.updates)(shortChannelId, extendedInfo_opt.map(_.array(idx)))) } - .filter(_.flag != 0) + val shortChannelIdAndFlags = shortChannelIds.array + .zipWithIndex + .map { case (shortChannelId: ShortChannelId, idx) => ShortChannelIdAndFlag(shortChannelId, computeFlag(d.channels, d.updates)(shortChannelId, extendedInfo_opt.map(_.array(idx)))) } + .filter(_.flag != 0) val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) { case ((c, u), ShortChannelIdAndFlag(_, flag)) => val c1 = c + (if (QueryFlagTypes.includeAnnouncement(flag)) 1 else 0) val u1 = u + (if (QueryFlagTypes.includeUpdate1(flag)) 1 else 0) + (if (QueryFlagTypes.includeUpdate2(flag)) 1 else 0) (c1, u1) } - log.info("received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", data.array.size, channelCount, updatesCount, data.encoding) + log.info("received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) val replies = shortChannelIdAndFlags .grouped(SHORTID_WINDOW) .map(chunk => QueryShortChannelIds(chainHash, - shortChannelIds = EncodedShortChannelIds(data.encoding, chunk.map(_.shortChannelId)), + shortChannelIds = EncodedShortChannelIds(shortChannelIds.encoding, chunk.map(_.shortChannelId)), queryFlags_opt = optionExtendedQueryFlags_opt map { - case _ => EncodedQueryFlags(data.encoding, chunk.map(_.flag)) + case _ => EncodedQueryFlags(shortChannelIds.encoding, chunk.map(_.flag)) })) .toList val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) @@ -556,24 +556,24 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, shortChannelIds, queryFlags_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (channelCount, updatesCount) = shortChannelIds.array - .zipWithIndex + .zipWithIndex .foldLeft((0, 0)) { - case ((c, u), (shortChannelId, idx)) => - var c1 = c - var u1 = u - val flag = queryFlags_opt.map(_.array(idx)).getOrElse((QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte) - d.channels.get(shortChannelId) match { - case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) - case Some(ca) => - if (QueryFlagTypes.includeAnnouncement(flag)) { - transport ! ca - c1 = c1 + 1 - } - if (QueryFlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } - if (QueryFlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } - } - (c1, u1) - } + case ((c, u), (shortChannelId, idx)) => + var c1 = c + var u1 = u + val flag = queryFlags_opt.map(_.array(idx)).getOrElse((QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte) + d.channels.get(shortChannelId) match { + case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) + case Some(ca) => + if (QueryFlagTypes.includeAnnouncement(flag)) { + transport ! ca + c1 = c1 + 1 + } + if (QueryFlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } + if (QueryFlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } + } + (c1, u1) + } log.info("received query_short_channel_ids with {} items, sent back {} channels and {} updates", shortChannelIds.array.size, channelCount, updatesCount) transport ! ReplyShortChannelIdsEnd(chainHash, 1) stay @@ -841,18 +841,18 @@ object Router { def computeFlag(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId, theirInfo_opt: Option[TimestampsAndChecksums]): Byte = { var flag = 0 - if (channels.contains(shortChannelId) || theirInfo_opt.isEmpty) { - val ourInfo = Router.getChannelDigestInfo(channels, updates)(shortChannelId) - val theirInfo = theirInfo_opt.get - // we request their channel_update if all those conditions are met: - // - it is more recent than ours - // - it is different from ours, or it is the same but ours is about to be stale - // - it is not stale itself - if (ourInfo.timestamp1 < theirInfo.timestamp1 && (ourInfo.checksum1 != theirInfo.checksum1 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp1)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 - if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 - } else { - // we don't know this channel: we request everything - QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + theirInfo_opt match { + case Some(theirInfo) if channels.contains(shortChannelId) => + val ourInfo = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + // we request their channel_update if all those conditions are met: + // - it is more recent than ours + // - it is different from ours, or it is the same but ours is about to be stale + // - it is not stale itself + if (ourInfo.timestamp1 < theirInfo.timestamp1 && (ourInfo.checksum1 != theirInfo.checksum1 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp1)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 + if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + case _ => + // we don't know this channel: we request everything + flag = QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 } flag.toByte } @@ -916,7 +916,11 @@ object Router { val timestamp2 = u2_opt.map(_.timestamp).getOrElse(0L) val checksum1 = u1_opt.map(getChecksum).getOrElse(0L) val checksum2 = u2_opt.map(getChecksum).getOrElse(0L) - TimestampsAndChecksums(timestamp1, timestamp2, checksum1, checksum2) + TimestampsAndChecksums( + timestamp1 = timestamp1, + checksum1 = checksum1, + timestamp2 = timestamp2, + checksum2 = checksum2) } def getChecksum(u: ChannelUpdate): Long = { 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 6325b47e15..b726922cf9 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 @@ -332,11 +332,15 @@ object LightningMessageCodecs { ("complete" | byte) ).as[ReplyShortChannelIdsEnd] + val extendedQueryFlagsCodec: Codec[ExtendedQueryFlags] = + discriminated[ExtendedQueryFlags].by(byte) + .typecase(1, provide(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) + val queryChannelRangeCodec: Codec[QueryChannelRange] = ( ("chainHash" | binarydata(32)) :: ("firstBlockNum" | uint32) :: ("numberOfBlocks" | uint32) :: - ("optionExtendedQueryFlags" | optional(bitsRemaining, byte)) + ("optionExtendedQueryFlags" | optional(bitsRemaining, extendedQueryFlagsCodec)) ).as[QueryChannelRange] val timestampsAndChecksumsCodec: Codec[TimestampsAndChecksums] = ( @@ -354,7 +358,7 @@ object LightningMessageCodecs { ("numberOfBlocks" | uint32) :: ("complete" | byte) :: ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: - ("optionExtendedQueryFlags_opt" | optional(bitsRemaining, byte)) :: + ("optionExtendedQueryFlags_opt" | optional(bitsRemaining, extendedQueryFlagsCodec)) :: ("extendedInfo_opt" | optional(bitsRemaining, variableSizeBytes(uint16, extendedInfoCodec))) ).as[ReplyChannelRange] 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 65744cda98..097c04c1c4 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 @@ -254,21 +254,24 @@ case class QueryShortChannelIds(chainHash: BinaryData, case class ReplyShortChannelIdsEnd(chainHash: BinaryData, complete: Byte) extends RoutingMessage with HasChainHash -case object ExtendedQueryFlags { - val TIMESTAMPS_AND_CHECKSUMS: Byte = 1 +// @formatter:off +sealed trait ExtendedQueryFlags +object ExtendedQueryFlags { + case object TIMESTAMPS_AND_CHECKSUMS extends ExtendedQueryFlags } +// @formatter:on case class QueryChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, - optionExtendedQueryFlags_opt: Option[Byte]) extends RoutingMessage with HasChainHash + optionExtendedQueryFlags_opt: Option[ExtendedQueryFlags]) extends RoutingMessage with HasChainHash case class ReplyChannelRange(chainHash: BinaryData, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, shortChannelIds: EncodedShortChannelIds, - optionExtendedQueryFlags_opt: Option[Byte], + optionExtendedQueryFlags_opt: Option[ExtendedQueryFlags], extendedInfo_opt: Option[ExtendedInfo]) extends RoutingMessage with HasChainHash case class GossipTimestampFilter(chainHash: BinaryData, From 7d7260011fc1a6c5957c6e8752dd30c92322283a Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 11 Mar 2019 12:24:09 +0100 Subject: [PATCH 35/86] formatting --- .../scala/fr/acinq/eclair/router/Router.scala | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) 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 d6af5f3f95..20aa821e22 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 @@ -74,10 +74,7 @@ case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChan } case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed) case class LiftChannelExclusion(desc: ChannelDesc) - -// channel queries as specified in BOLT 1.0 case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef, flags_opt: Option[ExtendedQueryFlags]) - case object GetRoutingState case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) @@ -580,7 +577,24 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsEnd), d) => sender ! TransportHandler.ReadAck(routingMessage) - stay using handleSyncEnd(d, remoteNodeId, transport) + // have we more channels to ask this peer? + val sync1 = d.sync.get(remoteNodeId) match { + case Some(sync) => + sync.pending match { + case nextRequest +: rest => + log.info(s"asking for the next slice of short_channel_ids (remaining=${sync.pending.size}/${sync.total})") + transport ! nextRequest + d.sync + (remoteNodeId -> sync.copy(pending = rest)) + case Nil => + // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove + // the remote peer from our map + log.info(s"sync complete (total=${sync.total})") + d.sync - remoteNodeId + } + case _ => d.sync + } + context.system.eventStream.publish(syncProgress(sync1)) + stay using d.copy(sync = sync1) } @@ -733,27 +747,6 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom d } - def handleSyncEnd(d: Data, remoteNodeId: PublicKey, transport: ActorRef): Data = { - // have we more channels to ask this peer? - val sync1 = d.sync.get(remoteNodeId) match { - case Some(sync) => - sync.pending match { - case nextRequest +: rest => - log.info(s"asking for the next slice of short_channel_ids (remaining=${sync.pending.size}/${sync.total})") - transport ! nextRequest - d.sync + (remoteNodeId -> sync.copy(pending = rest)) - case Nil => - // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove - // the remote peer from our map - log.info(s"sync complete (total=${sync.total})") - d.sync - remoteNodeId - } - case _ => d.sync - } - context.system.eventStream.publish(syncProgress(sync1)) - d.copy(sync = sync1) - } - override def mdc(currentMessage: Any): MDC = currentMessage match { case SendChannelQuery(remoteNodeId, _, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) case PeerRoutingMessage(_, remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) From d3a1d19c9894388c2e6985f31b7bfcdac9cf5ad1 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 11 Mar 2019 12:34:22 +0100 Subject: [PATCH 36/86] fixed `QueryFlagTypes` --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 4 ++-- .../fr/acinq/eclair/wire/LightningMessageTypes.scala | 8 ++++---- .../eclair/router/RoutingSyncWithChecksumsSpec.scala | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) 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 20aa821e22..0b7218fd4a 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 @@ -558,7 +558,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case ((c, u), (shortChannelId, idx)) => var c1 = c var u1 = u - val flag = queryFlags_opt.map(_.array(idx)).getOrElse((QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte) + val flag = queryFlags_opt.map(_.array(idx)).getOrElse((QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte) d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => @@ -845,7 +845,7 @@ object Router { if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 case _ => // we don't know this channel: we request everything - flag = QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + flag = QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 } flag.toByte } 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 097c04c1c4..001667a35f 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 @@ -230,11 +230,11 @@ case object EncodingTypes { } case object QueryFlagTypes { - val INCLUDE_CHANNEL_UPDATE_1: Byte = 1 - val INCLUDE_CHANNEL_UPDATE_2: Byte = 2 - val INCLUDE_ANNOUNCEMENT: Byte = 4 + val INCLUDE_CHANNEL_ANNOUNCEMENT: Byte = 1 + val INCLUDE_CHANNEL_UPDATE_1: Byte = 2 + val INCLUDE_CHANNEL_UPDATE_2: Byte = 4 - def includeAnnouncement(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_ANNOUNCEMENT) != 0 + def includeAnnouncement(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT) != 0 def includeUpdate1(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1) != 0 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala index 8dec898242..8bdf813a1c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala @@ -65,7 +65,7 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block)) // router should ask for our first block of ids - val shortChannelIdAndFlags = block.shortChannelIds.array.map(shortChannelId => ShortChannelIdAndFlag(shortChannelId, (QueryFlagTypes.INCLUDE_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte)) + val shortChannelIdAndFlags = block.shortChannelIds.array.map(shortChannelId => ShortChannelIdAndFlag(shortChannelId, (QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte)) val shortChannelIdAndFlags1 = shortChannelIdAndFlags.take(Router.SHORTID_WINDOW) assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags1.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags1.map(_.flag))))) From 81023a2aaa78581006a31bb07d06d488e91b03c8 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 11 Mar 2019 14:37:00 +0100 Subject: [PATCH 37/86] rework feature bits --- eclair-core/src/main/resources/reference.conf | 2 +- .../main/scala/fr/acinq/eclair/Features.scala | 43 ++++++++++++------- .../main/scala/fr/acinq/eclair/io/Peer.scala | 19 ++++---- .../eclair/wire/LightningMessageTypes.scala | 3 +- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 37 ++++++++++------ 5 files changed, 64 insertions(+), 40 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1a370f7fbc..cd9b05d1dc 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -43,7 +43,7 @@ eclair { node-color = "49daaa" global-features = "" - local-features = "80808a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_deprecated + option_channel_range_queries_extended + local-features = "0800008a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_extended override-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 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 1a943a6f3e..b9d7894f01 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -17,9 +17,7 @@ package fr.acinq.eclair -import java.util.BitSet - -import fr.acinq.bitcoin.BinaryData +import scodec.bits.{BitVector, ByteVector} /** @@ -36,26 +34,41 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 - def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) + val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY = 26 + val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL = 27 + + def apply(hex: String): Features = Features(ByteVector.fromValidHex(hex).toBitVector) +} + +case class Features(localFeatures: BitVector) { + + import Features._ + + def isSet(i: Int) = localFeatures.size > i && localFeatures.get(localFeatures.length - i - 1) + + def hasOptionDataLossProtectMandatory = isSet(OPTION_DATA_LOSS_PROTECT_MANDATORY) - def hasFeature(features: BinaryData, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit) + def hasOptionDataLossProtectOptional = isSet(OPTION_DATA_LOSS_PROTECT_OPTIONAL) + def hasInitialRoutingSync = isSet(INITIAL_ROUTING_SYNC_BIT_OPTIONAL) + + def hasChannelRangeQueriesMandatory = isSet(CHANNEL_RANGE_QUERIES_BIT_MANDATORY) + + def hasChannelRangeQueriesOptional = isSet(CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) + + def hasChannelRangeQueriesExtendedMandatory = isSet(CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY) + + def hasChannelRangeQueriesExtendedOptional = isSet(CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL) /** * 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(bitset: BitSet): Boolean = { - val supportedMandatoryFeatures = Set(OPTION_DATA_LOSS_PROTECT_MANDATORY) - for (i <- 0 until bitset.length() by 2) { - if (bitset.get(i) && !supportedMandatoryFeatures.contains(i)) return false + def areSupported: Boolean = { + val supportedMandatoryFeatures = Set(OPTION_DATA_LOSS_PROTECT_MANDATORY, CHANNEL_RANGE_QUERIES_BIT_MANDATORY, CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY) + for (i <- 0 until localFeatures.length.toInt by 2) { + if (localFeatures.reverse.get(i) && !supportedMandatoryFeatures.contains(i)) return false } return true } - - /** - * A feature set is supported if all even bits are supported. - * We just ignore unknown odd bits. - */ - def areSupported(features: BinaryData): Boolean = areSupported(BitSet.valueOf(features.reverse.toArray)) } 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 66af24105d..5c3684918b 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 @@ -31,6 +31,7 @@ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} import scodec.Attempt +import scodec.bits.ByteVector import scala.compat.Platform import scala.concurrent.duration._ @@ -117,27 +118,29 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(INITIALIZING) { case Event(remoteInit: wire.Init, d: InitializingData) => d.transport ! TransportHandler.ReadAck(remoteInit) - val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) + val remoteFeatures = Features(ByteVector(remoteInit.localFeatures.toArray).toBitVector) - log.info(s"peer has globalFeatures=${remoteInit.globalFeatures} localFeatures=${remoteInit.localFeatures}: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") - if (Features.areSupported(remoteInit.localFeatures)) { + log.info(s"peer has globalFeatures=${remoteInit.globalFeatures} localFeatures=${remoteInit.localFeatures}: initialRoutingSync=${remoteFeatures.hasInitialRoutingSync} channelRangeQueriesOptional=${remoteFeatures.hasChannelRangeQueriesOptional} channelRangeQueriesMandatory=${remoteFeatures.hasChannelRangeQueriesMandatory} channelRangeQueriesExtendedOptional=${remoteFeatures.hasChannelRangeQueriesExtendedOptional} channelRangeQueriesExtendedMandatory=${remoteFeatures.hasChannelRangeQueriesExtendedMandatory}") + if (remoteFeatures.areSupported) { d.origin_opt.foreach(origin => origin ! "connected") - if (remoteHasInitialRoutingSync) { - if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + if (remoteFeatures.hasInitialRoutingSync) { + if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory || remoteFeatures.hasChannelRangeQueriesExtendedOptional || remoteFeatures.hasChannelRangeQueriesExtendedMandatory) { // if they support channel queries we do nothing, they will send us their filters log.info("peer has set initial routing sync and supports channel range queries, we do nothing (they will send us a query)") } else { // "old" nodes, do as before + log.info("peer requested a full routing table dump") router ! GetRoutingState } } - if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { + if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = None) + } else if (remoteFeatures.hasChannelRangeQueriesExtendedOptional || remoteFeatures.hasChannelRangeQueriesExtendedMandatory) { + // if they support channel queries, always ask for their filter + router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) } // let's bring existing/requested channels online 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 001667a35f..2336695939 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 @@ -18,12 +18,11 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} -import com.google.common.net.HostAndPort import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar} import fr.acinq.eclair.{ShortChannelId, UInt64} -import scala.util.{Success, Try} +import scala.util.Try /** * Created by PM on 15/11/2016. 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 c629470ffd..92370fcf78 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -16,11 +16,8 @@ package fr.acinq.eclair -import java.nio.ByteOrder - -import fr.acinq.bitcoin.Protocol -import fr.acinq.eclair.Features._ import org.scalatest.FunSuite +import scodec.bits._ /** * Created by PM on 27/01/2017. @@ -29,25 +26,37 @@ import org.scalatest.FunSuite class FeaturesSpec extends FunSuite { test("'initial_routing_sync' feature") { - assert(hasFeature("08", Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) + assert(Features("08").hasInitialRoutingSync) } test("'data_loss_protect' feature") { - assert(hasFeature("01", Features.OPTION_DATA_LOSS_PROTECT_MANDATORY)) - assert(hasFeature("02", Features.OPTION_DATA_LOSS_PROTECT_OPTIONAL)) + assert(Features("01").hasOptionDataLossProtectMandatory) + assert(Features("02").hasOptionDataLossProtectOptional) } test("'initial_routing_sync' and 'data_loss_protect' feature") { - val features = "0a" - assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) + val features = Features("0a") + assert(features.areSupported && features.hasOptionDataLossProtectOptional && features.hasInitialRoutingSync) + } + + test("'initial_routing_sync' + 'data_loss_protect' + 'channel_range_queries' + 'channel_range_queries_extended'") { + val features = Features("0800008a") + assert(features.areSupported + && features.hasOptionDataLossProtectOptional + && features.hasInitialRoutingSync + && features.hasChannelRangeQueriesOptional + && features.hasChannelRangeQueriesExtendedOptional + ) } test("features compatibility") { - assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported(Protocol.writeUInt64(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY, ByteOrder.BIG_ENDIAN))) - assert(areSupported(Protocol.writeUInt64(1l << OPTION_DATA_LOSS_PROTECT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported("14") == false) - assert(areSupported("0141") == false) + assert(Features(bin"1000").areSupported) + assert(Features(bin"1").areSupported) + assert(Features(bin"10").areSupported) + assert(Features(bin"10000000000000000000000000000000000000").areSupported) + assert(Features(bin"100000000000000000000000000000000000000").areSupported == false) + assert(Features(bin"10100").areSupported == false) + assert(Features(bin"101000001").areSupported == false) } } From 564c1c527b542588cfbef6a73fa6165126100396 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 11 Mar 2019 19:25:35 +0100 Subject: [PATCH 38/86] send extended queries if available --- eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 5c3684918b..1f4ab91bf6 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 @@ -135,12 +135,12 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } } - if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory) { - // if they support channel queries, always ask for their filter - router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = None) - } else if (remoteFeatures.hasChannelRangeQueriesExtendedOptional || remoteFeatures.hasChannelRangeQueriesExtendedMandatory) { + if (remoteFeatures.hasChannelRangeQueriesExtendedOptional || remoteFeatures.hasChannelRangeQueriesExtendedMandatory) { // if they support channel queries, always ask for their filter router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) + } else if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory) { + // if they support channel queries, always ask for their filter + router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = None) } // let's bring existing/requested channels online From 5bbbebb41f8db06430fcfab60da428a983ea7ec6 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 11 Mar 2019 19:26:53 +0100 Subject: [PATCH 39/86] better logs --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 2 +- .../scala/fr/acinq/eclair/wire/LightningMessageTypes.scala | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 0b7218fd4a..2a4bac852a 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 @@ -534,7 +534,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom val u1 = u + (if (QueryFlagTypes.includeUpdate1(flag)) 1 else 0) + (if (QueryFlagTypes.includeUpdate2(flag)) 1 else 0) (c1, u1) } - log.info("received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) + log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={} queryFlags=${optionExtendedQueryFlags_opt.getOrElse("n/a")}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) val replies = shortChannelIdAndFlags .grouped(SHORTID_WINDOW) 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 2336695939..db586d8d2f 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 @@ -271,7 +271,9 @@ case class ReplyChannelRange(chainHash: BinaryData, complete: Byte, shortChannelIds: EncodedShortChannelIds, optionExtendedQueryFlags_opt: Option[ExtendedQueryFlags], - extendedInfo_opt: Option[ExtendedInfo]) extends RoutingMessage with HasChainHash + extendedInfo_opt: Option[ExtendedInfo]) extends RoutingMessage with HasChainHash { + extendedInfo_opt.foreach(extendedInfo => require(shortChannelIds.array.size == extendedInfo.array.size, s"shortChannelIds.size=${shortChannelIds.array.size} != extendedInfo.size=${extendedInfo.array.size}")) +} case class GossipTimestampFilter(chainHash: BinaryData, firstTimestamp: Long, From 7b40799f629159f0903f567172f79cf8e4aa1f2b Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 12 Mar 2019 10:00:10 +0100 Subject: [PATCH 40/86] typed EncodingType --- .../scala/fr/acinq/eclair/router/Router.scala | 4 ++-- .../eclair/wire/LightningMessageCodecs.scala | 8 +++---- .../eclair/wire/LightningMessageTypes.scala | 15 +++++++------ .../scala/fr/acinq/eclair/io/PeerSpec.scala | 4 ++-- .../acinq/eclair/router/RoutingSyncSpec.scala | 8 +++---- .../router/RoutingSyncWithChecksumsSpec.scala | 4 ++-- .../wire/LightningMessageCodecsSpec.scala | 22 ++++++++++++++----- 7 files changed, 38 insertions(+), 27 deletions(-) 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 2a4bac852a..7edb8c9a55 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 @@ -515,7 +515,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom .foreach(chunk => transport ! ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, - shortChannelIds = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, chunk.shortChannelIds), + shortChannelIds = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, chunk.shortChannelIds), optionExtendedQueryFlags_opt = optionExtendedQueryFlags_opt, extendedInfo_opt = optionExtendedQueryFlags_opt map { case ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS => ExtendedInfo(chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates))) @@ -725,7 +725,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // when we're sending updates to ourselves (transport_opt, remoteNodeId_opt) match { case (Some(transport), Some(remoteNodeId)) => - val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(u.shortChannelId)), queryFlags_opt = None) + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(u.shortChannelId)), queryFlags_opt = None) d.sync.get(remoteNodeId) match { case Some(sync) => // we already have a pending request to that node, let's add this channel to the list and we'll get it later 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 b726922cf9..5541e0c3a5 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 @@ -313,13 +313,13 @@ object LightningMessageCodecs { val encodedShortChannelIdsCodec: Codec[EncodedShortChannelIds] = discriminated[EncodedShortChannelIds].by(byte) - .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) - .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedShortChannelIds(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) + .\(0) { case a@EncodedShortChannelIds(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) + .\(1) { case a@EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) val encodedQueryFlagsCodec: Codec[EncodedQueryFlags] = discriminated[EncodedQueryFlags].by(byte) - .\(EncodingTypes.UNCOMPRESSED) { case a@EncodedQueryFlags(EncodingTypes.UNCOMPRESSED, _) => a }((provide(EncodingTypes.UNCOMPRESSED) :: list(byte)).as[EncodedQueryFlags]) - .\(EncodingTypes.COMPRESSED_ZLIB) { case a@EncodedQueryFlags(EncodingTypes.COMPRESSED_ZLIB, _) => a }((provide(EncodingTypes.COMPRESSED_ZLIB) :: zlib(list(byte))).as[EncodedQueryFlags]) + .\(0) { case a@EncodedQueryFlags(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(byte)).as[EncodedQueryFlags]) + .\(1) { case a@EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(byte))).as[EncodedQueryFlags]) val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( ("chainHash" | binarydata(32)) :: 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 db586d8d2f..8a7df8cb3a 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 @@ -221,12 +221,13 @@ case class PerHopPayload(shortChannelId: ShortChannelId, amtToForward: Long, outgoingCltvValue: Long) -// BOLT 1.0 channel queries - -case object EncodingTypes { - val UNCOMPRESSED: Byte = 0 - val COMPRESSED_ZLIB: Byte = 1 +// @formatter:off +sealed trait EncodingType +object EncodingType { + case object UNCOMPRESSED extends EncodingType + case object COMPRESSED_ZLIB extends EncodingType } +// @formatter:on case object QueryFlagTypes { val INCLUDE_CHANNEL_ANNOUNCEMENT: Byte = 1 @@ -240,10 +241,10 @@ case object QueryFlagTypes { def includeUpdate2(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2) != 0 } -case class EncodedShortChannelIds(encoding: Byte, +case class EncodedShortChannelIds(encoding: EncodingType, array: List[ShortChannelId]) -case class EncodedQueryFlags(encoding: Byte, +case class EncodedQueryFlags(encoding: EncodingType, array: List[Byte]) case class QueryShortChannelIds(chainHash: BinaryData, 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 b0253fa655..4fdb30eb84 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 @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements, SendPing} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo import fr.acinq.eclair.router.{ChannelRangeQueriesSpec, Rebroadcast} -import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingTypes, Error, Ping, Pong} +import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingType, Error, Ping, Pong} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, randomBytes, wire} import org.scalatest.Outcome @@ -150,7 +150,7 @@ class PeerSpec extends TestkitBaseClass { val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) - val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42000))), queryFlags_opt = None) + val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42000))), queryFlags_opt = None) // make sure that routing messages go through for (ann <- channels ++ updates) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index c7f93274ed..d7aa96f6c2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -53,8 +53,8 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.expectMsgType[GossipTimestampFilter] // split our answer in 3 blocks - val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) - val block2 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList), None, None) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) + val block2 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList), None, None) // send first block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) @@ -108,7 +108,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) // send first block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) @@ -128,7 +128,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { test("sync progress") { - def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(42))), None) + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42))), None) val nodeidA = randomKey.publicKey val nodeidB = randomKey.publicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala index 8bdf813a1c..7c218de78f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala @@ -58,7 +58,7 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun // send back all our ids and timestamps val block = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, - shortChannelIds = EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList), + shortChannelIds = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.toList), optionExtendedQueryFlags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), extendedInfo_opt = Some(ExtendedInfo(shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, initChannelUpdates)))) ) @@ -150,7 +150,7 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun // send back all our ids and timestamps val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, - EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, shortChannelIds.toList), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.toList), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, recentChannelUpdates))))) sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) 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 3f22f2017e..125b075271 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 @@ -27,10 +27,7 @@ import fr.acinq.eclair.wire.LightningMessageCodecs._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomBytes, randomKey} import org.scalatest.FunSuite import scodec.bits.{BitVector, ByteVector, HexStringSyntax} -import scodec.Codec -import scodec.codecs._ - -import scala.collection.SortedMap +import scodec.bits._ /** * Created by PM on 31/05/2016. @@ -241,9 +238,9 @@ class LightningMessageCodecsSpec extends FunSuite { val channel_update = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, 4, 5, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes(32), ShortChannelId(42), randomSignature, randomSignature) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None) + val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None) val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingTypes.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(1, 1, 1, 1), TimestampsAndChecksums(2, 2, 2, 2), TimestampsAndChecksums(3, 3, 3, 3))))) + val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(1, 1, 1, 1), TimestampsAndChecksums(2, 2, 2, 2), TimestampsAndChecksums(3, 3, 3, 3))))) val ping = Ping(100, BinaryData("01" * 10)) val pong = Pong(BinaryData("01" * 10)) val channel_reestablish = ChannelReestablish(randomBytes(32), 242842L, 42L) @@ -262,6 +259,19 @@ class LightningMessageCodecsSpec extends FunSuite { } } + test("non-reg encoding type") { + val refs = Map( + hex"0105 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4000400010204" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3000c01789c6364620100000e0008" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) + ) + refs.forall { + case (bin, obj) => + lightningMessageCodec.decode(bin.toBitVector).require.value == obj && lightningMessageCodec.encode(obj).require == bin.toBitVector + } + } + test("encode/decode per-hop payload") { val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) val bin = LightningMessageCodecs.perHopPayloadCodec.encode(payload).require From 4ba355ef5d9f2c3f55534a0d172a067250f3093d Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 12 Mar 2019 12:22:52 +0100 Subject: [PATCH 41/86] removed extended feature bit It is not needed since old implementations will just ignore additional fields. --- eclair-core/src/main/resources/reference.conf | 2 +- .../src/main/scala/fr/acinq/eclair/Features.scala | 9 +-------- .../src/main/scala/fr/acinq/eclair/io/Peer.scala | 10 ++++------ .../src/test/scala/fr/acinq/eclair/FeaturesSpec.scala | 10 ---------- 4 files changed, 6 insertions(+), 25 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index cd9b05d1dc..63863ca1a3 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -43,7 +43,7 @@ eclair { node-color = "49daaa" global-features = "" - local-features = "0800008a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_extended + local-features = "8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries override-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 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 b9d7894f01..465e9775fa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -34,9 +34,6 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 - val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY = 26 - val CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL = 27 - def apply(hex: String): Features = Features(ByteVector.fromValidHex(hex).toBitVector) } @@ -56,16 +53,12 @@ case class Features(localFeatures: BitVector) { def hasChannelRangeQueriesOptional = isSet(CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) - def hasChannelRangeQueriesExtendedMandatory = isSet(CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY) - - def hasChannelRangeQueriesExtendedOptional = isSet(CHANNEL_RANGE_QUERIES_EXTENDED_BIT_OPTIONAL) - /** * 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: Boolean = { - val supportedMandatoryFeatures = Set(OPTION_DATA_LOSS_PROTECT_MANDATORY, CHANNEL_RANGE_QUERIES_BIT_MANDATORY, CHANNEL_RANGE_QUERIES_EXTENDED_BIT_MANDATORY) + val supportedMandatoryFeatures = Set(OPTION_DATA_LOSS_PROTECT_MANDATORY, CHANNEL_RANGE_QUERIES_BIT_MANDATORY) for (i <- 0 until localFeatures.length.toInt by 2) { if (localFeatures.reverse.get(i) && !supportedMandatoryFeatures.contains(i)) return false } 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 1f4ab91bf6..7c694ed0bb 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 @@ -120,12 +120,12 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor d.transport ! TransportHandler.ReadAck(remoteInit) val remoteFeatures = Features(ByteVector(remoteInit.localFeatures.toArray).toBitVector) - log.info(s"peer has globalFeatures=${remoteInit.globalFeatures} localFeatures=${remoteInit.localFeatures}: initialRoutingSync=${remoteFeatures.hasInitialRoutingSync} channelRangeQueriesOptional=${remoteFeatures.hasChannelRangeQueriesOptional} channelRangeQueriesMandatory=${remoteFeatures.hasChannelRangeQueriesMandatory} channelRangeQueriesExtendedOptional=${remoteFeatures.hasChannelRangeQueriesExtendedOptional} channelRangeQueriesExtendedMandatory=${remoteFeatures.hasChannelRangeQueriesExtendedMandatory}") + log.info(s"peer has globalFeatures=${remoteInit.globalFeatures} localFeatures=${remoteInit.localFeatures}: initialRoutingSync=${remoteFeatures.hasInitialRoutingSync} channelRangeQueriesOptional=${remoteFeatures.hasChannelRangeQueriesOptional} channelRangeQueriesMandatory=${remoteFeatures.hasChannelRangeQueriesMandatory}") if (remoteFeatures.areSupported) { d.origin_opt.foreach(origin => origin ! "connected") if (remoteFeatures.hasInitialRoutingSync) { - if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory || remoteFeatures.hasChannelRangeQueriesExtendedOptional || remoteFeatures.hasChannelRangeQueriesExtendedMandatory) { + if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory) { // if they support channel queries we do nothing, they will send us their filters log.info("peer has set initial routing sync and supports channel range queries, we do nothing (they will send us a query)") } else { @@ -135,12 +135,10 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } } - if (remoteFeatures.hasChannelRangeQueriesExtendedOptional || remoteFeatures.hasChannelRangeQueriesExtendedMandatory) { + if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter + // NB: we always add extended info; if peer doesn't understand them it will ignore them router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) - } else if (remoteFeatures.hasChannelRangeQueriesOptional || remoteFeatures.hasChannelRangeQueriesMandatory) { - // if they support channel queries, always ask for their filter - router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = None) } // let's bring existing/requested channels online 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 92370fcf78..0ca108fc98 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -39,16 +39,6 @@ class FeaturesSpec extends FunSuite { assert(features.areSupported && features.hasOptionDataLossProtectOptional && features.hasInitialRoutingSync) } - test("'initial_routing_sync' + 'data_loss_protect' + 'channel_range_queries' + 'channel_range_queries_extended'") { - val features = Features("0800008a") - assert(features.areSupported - && features.hasOptionDataLossProtectOptional - && features.hasInitialRoutingSync - && features.hasChannelRangeQueriesOptional - && features.hasChannelRangeQueriesExtendedOptional - ) - } - test("features compatibility") { assert(Features(bin"1000").areSupported) assert(Features(bin"1").areSupported) From f5951cc21707e1652458ceba09b377b0f144e706 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 14 Mar 2019 10:45:10 +0100 Subject: [PATCH 42/86] wip --- .../scala/fr/acinq/eclair/router/Router.scala | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) 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 4c593daafe..08373a0eac 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 @@ -57,6 +57,11 @@ case class RouterConf(randomizeRouteSelection: Boolean, searchRatioChannelCapacity: Double) case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) +case class ChannelData(ann: ChannelAnnouncement, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate]) { + def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) ann.nodeId1 else ann.nodeId2 + def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt + def updateSameSideAs(u: ChannelUpdate): ChannelData = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) +} case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) case class RouteParams(randomize: Boolean, maxFeeBaseMsat: Long, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: Int, ratios: Option[WeightRatios]) case class RouteRequest(source: PublicKey, @@ -74,15 +79,14 @@ case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a Tempo case class LiftChannelExclusion(desc: ChannelDesc) case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) case object GetRoutingState -case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) +case class RoutingState(channels: Iterable[ChannelData], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int) case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, ChannelAnnouncement], - updates: Map[ChannelDesc, ChannelUpdate], + channels: SortedMap[ShortChannelId, ChannelData], stash: Stash, rebroadcast: Rebroadcast, awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done @@ -144,27 +148,30 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom val nodes = db.listNodes() val updates = db.listChannelUpdates() log.info("loaded from db: channels={} nodes={} updates={}", channels.size, nodes.size, updates.size) - val initChannels = channels.keys.foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = updates.map { u => - val desc = getDesc(u, initChannels(u.shortChannelId)) - desc -> u - }.toMap + // TODO + val initChannels = SortedMap.empty[ShortChannelId, ChannelData] +// val initChannels = channels.keys.foldLeft(TreeMap.empty[ShortChannelId, ChannelData]) { case (m, c) => m + (c.shortChannelId -> c) } +// val initChannelUpdates = updates.map { u => +// val desc = getDesc(u, initChannels(u.shortChannelId)) +// desc -> u +// }.toMap // this will be used to calculate routes - val graph = DirectedGraph.makeGraph(initChannelUpdates) + val graph = DirectedGraph.makeGraph(Map.empty) + //val graph = DirectedGraph.makeGraph(initChannelUpdates) val initNodes = nodes.map(n => (n.nodeId -> n)).toMap // send events for remaining channels/nodes - context.system.eventStream.publish(ChannelsDiscovered(initChannels.values.map(c => SingleChannelDiscovered(c, channels(c)._2)))) - context.system.eventStream.publish(ChannelUpdatesReceived(initChannelUpdates.values)) + context.system.eventStream.publish(ChannelsDiscovered(initChannels.values.map(c => SingleChannelDiscovered(c.ann, channels(c)._2)))) + //context.system.eventStream.publish(ChannelUpdatesReceived(initChannelUpdates.values)) context.system.eventStream.publish(NodesDiscovered(initNodes.values)) // watch the funding tx of all these channels // note: some of them may already have been spent, in that case we will receive the watch event immediately - initChannels.values.foreach { c => - val txid = channels(c)._1 - val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) - val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) - watcher ! WatchSpentBasic(self, txid, outputIndex, fundingOutputScript, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId)) - } +// initChannels.values.foreach { c => +// val txid = channels(c)._1 +// val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) +// val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) +// watcher ! WatchSpentBasic(self, txid, outputIndex, fundingOutputScript, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId)) +// } // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored @@ -173,7 +180,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom log.info(s"initialization completed, ready to process messages") Try(initialized.map(_.success(Done))) - startWith(NORMAL, Data(initNodes, initChannels, initChannelUpdates, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, privateUpdates = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) + startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, privateUpdates = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) } when(NORMAL) { @@ -228,7 +235,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(GetRoutingState, d: Data) => log.info(s"getting valid announcements for $sender") - sender ! RoutingState(d.channels.values, d.updates.values, d.nodes.values) + sender ! RoutingState(d.channels.values, d.nodes.values) stay case Event(v@ValidateResult(c, _), d0) => @@ -295,7 +302,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // note: if the channel is graduating from private to public, the implementation (in the LocalChannelUpdate handler) guarantees that we will process a new channel_update // right after the channel_announcement, channel_updates will be moved from private to public at that time val d1 = d0.copy( - channels = d0.channels + (c.shortChannelId -> c), + channels = d0.channels + (c.shortChannelId -> ChannelData(c, None, None)), privateChannels = d0.privateChannels - c.shortChannelId, // we remove fake announcements that we may have made before rebroadcast = d0.rebroadcast.copy(channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet)), // we also add the newly validated channels to the rebroadcast queue stash = stash1, @@ -313,7 +320,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d) if d.channels.contains(shortChannelId) => - val lostChannel = d.channels(shortChannelId) + val lostChannel = d.channels(shortChannelId).ann log.info("funding tx of channelId={} has been spent", shortChannelId) // we need to remove nodes that aren't tied to any channels anymore val channels1 = d.channels - lostChannel.shortChannelId @@ -333,7 +340,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom db.removeNode(nodeId) context.system.eventStream.publish(NodeLost(nodeId)) } - stay using d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, updates = d.updates.filterKeys(_.shortChannelId != shortChannelId), graph = graph1) + stay using d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graph = graph1) case Event(TickBroadcast, d) => if (d.rebroadcast.channels.isEmpty && d.rebroadcast.updates.isEmpty && d.rebroadcast.nodes.isEmpty) { @@ -347,28 +354,27 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(TickPruneStaleChannels, d) => // first we select channels that we will prune - val staleChannels = getStaleChannels(d.channels.values, d.updates) - // then we clean up the related channel updates - val staleUpdates = staleChannels.map(d.channels).flatMap(c => Seq(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2), ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1))) - // finally we remove nodes that aren't tied to any channels anymore (and deduplicate them) - val potentialStaleNodes = staleChannels.map(d.channels).flatMap(c => Set(c.nodeId1, c.nodeId2)).toSet - val channels1 = d.channels -- staleChannels + val staleChannels = getStaleChannels(d.channels.values) + val staleChannelIds = staleChannels.map(_.ann.shortChannelId) + // then we remove nodes that aren't tied to any channels anymore (and deduplicate them) + val potentialStaleNodes = staleChannels.flatMap(c => Set(c.ann.nodeId1, c.ann.nodeId2)).toSet + val channels1 = d.channels -- staleChannelIds // no need to iterate on all nodes, just on those that are affected by current pruning val staleNodes = potentialStaleNodes.filterNot(nodeId => hasChannels(nodeId, channels1.values)) // let's clean the db and send the events - db.removeChannels(staleChannels) // NB: this also removes channel updates + db.removeChannels(staleChannelIds) // NB: this also removes channel updates // we keep track of recently pruned channels so we don't revalidate them (zombie churn) - db.addToPruned(staleChannels) - staleChannels.foreach { shortChannelId => + db.addToPruned(staleChannelIds) + staleChannelIds.foreach { shortChannelId => log.info("pruning shortChannelId={} (stale)", shortChannelId) context.system.eventStream.publish(ChannelLost(shortChannelId)) } val staleChannelsToRemove = new mutable.MutableList[ChannelDesc] - staleChannels.map(d.channels).foreach(ca => { - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2) - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1) + staleChannels.foreach(ca => { + staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId1, ca.ann.nodeId2) + staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId2, ca.ann.nodeId1) }) val graph1 = d.graph.removeEdges(staleChannelsToRemove) @@ -378,7 +384,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom db.removeNode(nodeId) context.system.eventStream.publish(NodeLost(nodeId)) } - stay using d.copy(nodes = d.nodes -- staleNodes, channels = channels1, updates = d.updates -- staleUpdates, graph = graph1) + stay using d.copy(nodes = d.nodes -- staleNodes, channels = channels1, graph = graph1) case Event(ExcludeChannel(desc@ChannelDesc(shortChannelId, nodeId, _)), d) => val banDuration = nodeParams.routerConf.channelExcludeDuration @@ -399,12 +405,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom stay case Event('updates, d) => - sender ! (d.updates ++ d.privateUpdates).values + val updates: Iterable[ChannelUpdate] = (d.channels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt)) ++ d.privateUpdates.values + sender ! updates stay - case Event('updatesMap, d) => - sender ! (d.updates ++ d.privateUpdates) - stay +// case Event('updatesMap, d) => +// val updatesMap +// sender ! (d.updates ++ d.privateUpdates) +// stay case Event('data, d) => sender ! d @@ -619,8 +627,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom if (d.channels.contains(u.shortChannelId)) { // related channel is already known (note: this means no related channel_update is in the stash) val publicChannel = true - val c = d.channels(u.shortChannelId) - val desc = getDesc(u, c) + val data = d.channels(u.shortChannelId) if (d.rebroadcast.updates.contains(u)) { log.debug("ignoring {} (pending rebroadcast)", u) val origins = d.rebroadcast.updates(u) + origin @@ -628,14 +635,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } else if (isStale(u)) { log.debug("ignoring {} (stale)", u) d - } else if (d.updates.contains(desc) && d.updates(desc).timestamp >= u.timestamp) { + } else if (data.getSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (duplicate)", u) d - } else if (!Announcements.checkSig(u, desc.a)) { + } else if (!Announcements.checkSig(u, data.getNodeIdSameSideAs(u))) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) origin ! InvalidSignature(u) d - } else if (d.updates.contains(desc)) { + } else if (data.getSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) db.updateChannelUpdate(u) @@ -644,14 +651,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case true => d.graph.removeEdge(desc).addEdge(desc, u) case false => d.graph.removeEdge(desc) // if the channel is now disabled, we remove it from the graph } - d.copy(updates = d.updates + (desc -> u), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) + d.copy(channels = d.channels + (u.shortChannelId -> data.updateSameSideAs(u)), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) db.addChannelUpdate(u) // we also need to update the graph val graph1 = d.graph.addEdge(desc, u) - d.copy(updates = d.updates + (desc -> u), privateUpdates = d.privateUpdates - desc, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) + d.copy(channels = d.channels + (u.shortChannelId -> data.updateSameSideAs(u)), privateUpdates = d.privateUpdates - desc, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) } } else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) { // channel is currently being validated @@ -755,7 +762,7 @@ object Router { def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2 - def hasChannels(nodeId: PublicKey, channels: Iterable[ChannelAnnouncement]): Boolean = channels.exists(c => isRelatedTo(c, nodeId)) + def hasChannels(nodeId: PublicKey, channels: Iterable[ChannelData]): Boolean = channels.exists(c => isRelatedTo(c.ann, nodeId)) def isStale(u: ChannelUpdate): Boolean = { // BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks (1209600 seconds)" @@ -783,14 +790,7 @@ object Router { blockHeight < staleThresholdBlocks && update1_opt.map(isStale).getOrElse(true) && update2_opt.map(isStale).getOrElse(true) } - def getStaleChannels(channels: Iterable[ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate]): Iterable[ShortChannelId] = { - val staleChannels = channels.filter { c => - val update1 = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) - val update2 = updates.get(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) - isStale(c, update1, update2) - } - staleChannels.map(_.shortChannelId) - } + def getStaleChannels(channels: Iterable[ChannelData]): Iterable[ChannelData] = channels.filter(data => isStale(data.ann, data.update_1_opt, data.update_2_opt)) /** * Filters channels that we want to send to nodes asking for a channel range From c2ebcac4987104c232a5ff952b097bb69ccb1211 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 14 Mar 2019 11:31:14 +0100 Subject: [PATCH 43/86] wip --- .../scala/fr/acinq/eclair/db/NetworkDb.scala | 12 +- .../scala/fr/acinq/eclair/router/Graph.scala | 3 +- .../scala/fr/acinq/eclair/router/Router.scala | 150 +++++++++--------- .../gui/controllers/AboutController.scala | 3 +- .../controllers/ChannelPaneController.scala | 8 +- .../gui/controllers/NodeInfoController.scala | 7 +- .../controllers/NotificationsController.scala | 5 +- .../ReceivePaymentController.scala | 13 +- .../controllers/SendPaymentController.scala | 9 +- .../gui/controllers/SplashController.scala | 3 +- 10 files changed, 106 insertions(+), 107 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index 24a16bb1a9..8f489c5124 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -19,8 +19,11 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.router.PublicChannel import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} +import scala.collection.immutable.SortedMap + trait NetworkDb { def addNode(n: NodeAnnouncement) @@ -35,20 +38,13 @@ trait NetworkDb { def removeChannel(shortChannelId: ShortChannelId) = removeChannels(Seq(shortChannelId)) - /** - * This method removes channel announcements and associated channel updates for a list of channel ids - * - * @param shortChannelIds list of short channel ids - */ def removeChannels(shortChannelIds: Iterable[ShortChannelId]) - def listChannels(): Map[ChannelAnnouncement, (ByteVector32, Satoshi)] - def addChannelUpdate(u: ChannelUpdate) def updateChannelUpdate(u: ChannelUpdate) - def listChannelUpdates(): Seq[ChannelUpdate] + def listChannels(): SortedMap[ShortChannelId, PublicChannel] def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 4392c757c4..a9e3189d67 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -23,6 +23,7 @@ import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.ChannelUpdate +import scala.collection.immutable.SortedMap import scala.collection.mutable object Graph { @@ -530,7 +531,7 @@ object Graph { } // optimized constructor - def makeGraph(descAndUpdates: Map[ChannelDesc, ChannelUpdate]): DirectedGraph = { + def makeGraph(channels: SortedMap[ShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization val mutableMap = new {} with mutable.HashMap[PublicKey, List[GraphEdge]] { 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 08373a0eac..3865a29d8c 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 @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import akka.Done import akka.actor.{ActorRef, Props, Status} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} import fr.acinq.eclair._ @@ -35,7 +35,7 @@ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ import scodec.bits.ByteVector -import scala.collection.immutable.{SortedMap, TreeMap} +import scala.collection.immutable.SortedMap import scala.collection.{SortedSet, mutable} import scala.compat.Platform import scala.concurrent.duration._ @@ -57,11 +57,21 @@ case class RouterConf(randomizeRouteSelection: Boolean, searchRatioChannelCapacity: Double) case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) -case class ChannelData(ann: ChannelAnnouncement, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate]) { + +case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate]) { def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) ann.nodeId1 else ann.nodeId2 def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt - def updateSameSideAs(u: ChannelUpdate): ChannelData = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) + def updateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) +} +case class PrivateChannel(nodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate])(implicit nodeParams: NodeParams) { + val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeParams.nodeId, nodeId)) (nodeParams.nodeId, nodeId) else (nodeId, nodeParams.nodeId) + def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) nodeId1 else nodeId2 + def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt + def updateSameSideAs(u: ChannelUpdate): PrivateChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) } + +case class AssistedChannel(extraHop: ExtraHop, nextNodeId: PublicKey) + case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) case class RouteParams(randomize: Boolean, maxFeeBaseMsat: Long, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: Int, ratios: Option[WeightRatios]) case class RouteRequest(source: PublicKey, @@ -79,19 +89,18 @@ case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a Tempo case class LiftChannelExclusion(desc: ChannelDesc) case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) case object GetRoutingState -case class RoutingState(channels: Iterable[ChannelData], nodes: Iterable[NodeAnnouncement]) +case class RoutingState(channels: Iterable[PublicChannel], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int) case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, ChannelData], + channels: SortedMap[ShortChannelId, PublicChannel], stash: Stash, rebroadcast: Rebroadcast, awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done - privateChannels: Map[ShortChannelId, PublicKey], // short_channel_id -> node_id - privateUpdates: Map[ChannelDesc, ChannelUpdate], + privateChannels: Map[ShortChannelId, PrivateChannel], // short_channel_id -> node_id excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure graph: DirectedGraph, sync: Map[PublicKey, Sync] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query @@ -114,6 +123,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom import Router._ + implicit val np = nodeParams + import ExecutionContext.Implicits.global context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) @@ -146,32 +157,24 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom log.info("loading network announcements from db...") val channels = db.listChannels() val nodes = db.listNodes() - val updates = db.listChannelUpdates() - log.info("loaded from db: channels={} nodes={} updates={}", channels.size, nodes.size, updates.size) - // TODO - val initChannels = SortedMap.empty[ShortChannelId, ChannelData] -// val initChannels = channels.keys.foldLeft(TreeMap.empty[ShortChannelId, ChannelData]) { case (m, c) => m + (c.shortChannelId -> c) } -// val initChannelUpdates = updates.map { u => -// val desc = getDesc(u, initChannels(u.shortChannelId)) -// desc -> u -// }.toMap + log.info("loaded from db: channels={} nodes={}", channels.size, nodes.size) + val initChannels = channels // this will be used to calculate routes - val graph = DirectedGraph.makeGraph(Map.empty) - //val graph = DirectedGraph.makeGraph(initChannelUpdates) + val graph = DirectedGraph.makeGraph(initChannels) val initNodes = nodes.map(n => (n.nodeId -> n)).toMap // send events for remaining channels/nodes - context.system.eventStream.publish(ChannelsDiscovered(initChannels.values.map(c => SingleChannelDiscovered(c.ann, channels(c)._2)))) + context.system.eventStream.publish(ChannelsDiscovered(initChannels.values.map(pc => SingleChannelDiscovered(pc.ann, pc.capacity)))) //context.system.eventStream.publish(ChannelUpdatesReceived(initChannelUpdates.values)) context.system.eventStream.publish(NodesDiscovered(initNodes.values)) // watch the funding tx of all these channels // note: some of them may already have been spent, in that case we will receive the watch event immediately -// initChannels.values.foreach { c => -// val txid = channels(c)._1 -// val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) -// val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) -// watcher ! WatchSpentBasic(self, txid, outputIndex, fundingOutputScript, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId)) -// } + initChannels.values.foreach { pc => + val txid = pc.fundingTxid + val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(pc.ann.shortChannelId) + val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(pc.ann.bitcoinKey1, pc.ann.bitcoinKey2))) + watcher ! WatchSpentBasic(self, txid, outputIndex, fundingOutputScript, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(pc.ann.shortChannelId)) + } // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored @@ -180,7 +183,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom log.info(s"initialization completed, ready to process messages") Try(initialized.map(_.success(Done))) - startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, privateUpdates = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) + startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) } when(NORMAL) { @@ -208,7 +211,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations) // let's create a corresponding private channel and process the channel_update log.info("adding unannounced local channel to remote={} shortChannelId={}", remoteNodeId, shortChannelId) - stay using handle(u, self, d.copy(privateChannels = d.privateChannels + (shortChannelId -> remoteNodeId))) + stay using handle(u, self, d.copy(privateChannels = d.privateChannels + (shortChannelId -> PrivateChannel(remoteNodeId, None, None)))) } } @@ -228,7 +231,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom .removeEdge(desc1) .removeEdge(desc2) // and we remove the channel and channel_update from our state - stay using d.copy(privateChannels = d.privateChannels - shortChannelId, privateUpdates = d.privateUpdates - desc1 - desc2, graph = graph1) + stay using d.copy(privateChannels = d.privateChannels - shortChannelId, graph = graph1) } else { stay } @@ -244,10 +247,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case _ => () } log.info("got validation result for shortChannelId={} (awaiting={} stash.nodes={} stash.updates={})", c.shortChannelId, d0.awaiting.size, d0.stash.nodes.size, d0.stash.updates.size) - val success = v match { + val publicChannel_opt = v match { case ValidateResult(c, Left(t)) => log.warning("validation failure for shortChannelId={} reason={}", c.shortChannelId, t.getMessage) - false + None case ValidateResult(c, Right((tx, UtxoStatus.Unspent))) => val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) // let's check that the output is indeed a P2WSH multisig 2-of-2 of nodeid1 and nodeid2) @@ -258,7 +261,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Some(origins) => origins.foreach(_ ! InvalidAnnouncement(c)) case _ => () } - false + None } else { watcher ! WatchSpentBasic(self, tx, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId)) // TODO: check feature bit set @@ -273,7 +276,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses) self ! nodeAnn } - true + Some(PublicChannel(c, tx.txid, capacity, None, None)) } case ValidateResult(c, Right((tx, fundingTxStatus: UtxoStatus.Spent))) => if (fundingTxStatus.spendingTxConfirmed) { @@ -288,7 +291,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } // there may be a record if we have just restarted db.removeChannel(c.shortChannelId) - false + None } // we also reprocess node and channel_update announcements related to channels that were just analyzed @@ -298,11 +301,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom val stash1 = d0.stash.copy(updates = d0.stash.updates -- reprocessUpdates.keys, nodes = d0.stash.nodes -- reprocessNodes.keys) // we remove channel from awaiting map val awaiting1 = d0.awaiting - c - if (success) { + + publicChannel_opt match { + case Some(pc) => // note: if the channel is graduating from private to public, the implementation (in the LocalChannelUpdate handler) guarantees that we will process a new channel_update // right after the channel_announcement, channel_updates will be moved from private to public at that time val d1 = d0.copy( - channels = d0.channels + (c.shortChannelId -> ChannelData(c, None, None)), + channels = d0.channels + (c.shortChannelId -> pc), privateChannels = d0.privateChannels - c.shortChannelId, // we remove fake announcements that we may have made before rebroadcast = d0.rebroadcast.copy(channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet)), // we also add the newly validated channels to the rebroadcast queue stash = stash1, @@ -315,7 +320,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case (d, (n, origins)) => origins.foldLeft(d) { case (d, origin) => handle(n, origin, d) } // we reprocess the same node_announcement for every origins (to preserve origin information) } stay using d3 - } else { + case None => stay using d0.copy(stash = stash1, awaiting = awaiting1) } @@ -401,11 +406,11 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom stay case Event('channels, d) => - sender ! d.channels.values + sender ! d.channels.values.map(_.ann) stay case Event('updates, d) => - val updates: Iterable[ChannelUpdate] = (d.channels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt)) ++ d.privateUpdates.values + val updates: Iterable[ChannelUpdate] = d.channels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt) ++ d.privateChannels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt) sender ! updates stay @@ -421,17 +426,16 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(RouteRequest(start, end, amount, assistedRoutes, ignoreNodes, ignoreChannels, params_opt), d) => // we convert extra routing info provided in the payment request to fake channel_update // it takes precedence over all other channel_updates we know - val assistedUpdates = assistedRoutes.flatMap(toFakeUpdates(_, end)).toMap + val assistedChannels: Map[ShortChannelId, AssistedChannel] = assistedRoutes.flatMap(toAssistedChannels(_, end)).toMap // we also filter out updates corresponding to channels/nodes that are blacklisted for this particular request // TODO: in case of duplicates, d.updates will be overridden by assistedUpdates even if they are more recent! - val ignoredUpdates = getIgnoredChannelDesc(d.updates ++ d.privateUpdates ++ assistedUpdates, ignoreNodes) ++ ignoreChannels ++ d.excludedChannels - val extraEdges = assistedUpdates.map { case (c, u) => GraphEdge(c, u) }.toSet + val extraEdges = assistedChannels.values.map(ac => GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop))).toSet val params = params_opt.getOrElse(defaultRouteParams) val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 - log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedUpdates.keys.mkString(","), ignoreNodes.map(_.toBin).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) + log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), ignoreNodes.map(_.toBin).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) log.info(s"finding a route with randomize={} params={}", routesToFind > 1, params) - findRoute(d.graph, start, end, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredUpdates.toSet, routeParams = params) + findRoute(d.graph, start, end, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoreChannels, ignoredVertices = ignoreNodes, routeParams = params) .map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels)) .recover { case t => sender ! Status.Failure(t) } stay @@ -514,7 +518,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom sender ! TransportHandler.ReadAck(routingMessage) log.info("received query_channel_range={}", routingMessage) // sort channel ids and keep the ones which are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels)) // TODO: we don't compress to be compatible with old mobile apps, switch to ZLIB ASAP // Careful: when we remove GZIP support, eclair-wallet 0.3.0 will stop working i.e. channels to ACINQ nodes will not // work anymore @@ -528,7 +532,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => sender ! TransportHandler.ReadAck(routingMessage) val (format, theirShortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) - val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) + val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels)) val missing: SortedSet[ShortChannelId] = theirShortChannelIds -- ourShortChannelIds log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={} useGzip={}", missing.size, format, useGzip) val d1 = if (missing.nonEmpty) { @@ -555,10 +559,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom shortChannelIds.foreach(shortChannelId => { d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) - case Some(ca) => - transport ! ca - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).map(u => transport ! u) - d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).map(u => transport ! u) + case Some(channelData) => + transport ! channelData.ann + channelData.update_1_opt.foreach(u => transport ! u) + channelData.update_2_opt.foreach(u => transport ! u) } }) transport ! ReplyShortChannelIdsEnd(chainHash, 1) @@ -608,7 +612,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom context.system.eventStream.publish(NodeUpdated(n)) db.updateNode(n) d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> Set(origin)))) - } else if (d.channels.values.exists(c => isRelatedTo(c, n.nodeId))) { + } else if (d.channels.values.exists(c => isRelatedTo(c.ann, n.nodeId))) { log.debug("added node nodeId={}", n.nodeId) context.system.eventStream.publish(NodesDiscovered(n :: Nil)) db.addNode(n) @@ -627,7 +631,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom if (d.channels.contains(u.shortChannelId)) { // related channel is already known (note: this means no related channel_update is in the stash) val publicChannel = true - val data = d.channels(u.shortChannelId) + val pc = d.channels(u.shortChannelId) + val desc = getDesc(u, pc.ann) if (d.rebroadcast.updates.contains(u)) { log.debug("ignoring {} (pending rebroadcast)", u) val origins = d.rebroadcast.updates(u) + origin @@ -635,14 +640,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } else if (isStale(u)) { log.debug("ignoring {} (stale)", u) d - } else if (data.getSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + } else if (pc.getSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (duplicate)", u) d - } else if (!Announcements.checkSig(u, data.getNodeIdSameSideAs(u))) { + } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) origin ! InvalidSignature(u) d - } else if (data.getSameSideAs(u).isDefined) { + } else if (pc.getSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) db.updateChannelUpdate(u) @@ -651,14 +656,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case true => d.graph.removeEdge(desc).addEdge(desc, u) case false => d.graph.removeEdge(desc) // if the channel is now disabled, we remove it from the graph } - d.copy(channels = d.channels + (u.shortChannelId -> data.updateSameSideAs(u)), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) + d.copy(channels = d.channels + (u.shortChannelId -> pc.updateSameSideAs(u)), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) db.addChannelUpdate(u) // we also need to update the graph val graph1 = d.graph.addEdge(desc, u) - d.copy(channels = d.channels + (u.shortChannelId -> data.updateSameSideAs(u)), privateUpdates = d.privateUpdates - desc, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) + d.copy(channels = d.channels + (u.shortChannelId -> pc.updateSameSideAs(u)), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) } } else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) { // channel is currently being validated @@ -672,31 +677,30 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } } else if (d.privateChannels.contains(u.shortChannelId)) { val publicChannel = false - val remoteNodeId = d.privateChannels(u.shortChannelId) - val (a, b) = if (Announcements.isNode1(nodeParams.nodeId, remoteNodeId)) (nodeParams.nodeId, remoteNodeId) else (remoteNodeId, nodeParams.nodeId) - val desc = if (Announcements.isNode1(u.channelFlags)) ChannelDesc(u.shortChannelId, a, b) else ChannelDesc(u.shortChannelId, b, a) + val pc = d.privateChannels(u.shortChannelId) + val desc = if (Announcements.isNode1(u.channelFlags)) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1) if (isStale(u)) { log.debug("ignoring {} (stale)", u) d - } else if (d.updates.contains(desc) && d.updates(desc).timestamp >= u.timestamp) { + } else if (pc.getSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (already know same or newer)", u) d } else if (!Announcements.checkSig(u, desc.a)) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) origin ! InvalidSignature(u) d - } else if (d.privateUpdates.contains(desc)) { + } else if (pc.getSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph val graph1 = d.graph.removeEdge(desc).addEdge(desc, u) - d.copy(privateUpdates = d.privateUpdates + (desc -> u), graph = graph1) + d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateSameSideAs(u)), graph = graph1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph val graph1 = d.graph.addEdge(desc, u) - d.copy(privateUpdates = d.privateUpdates + (desc -> u), graph = graph1) + d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateSameSideAs(u)), graph = graph1) } } else if (db.isPruned(u.shortChannelId) && !isStale(u)) { // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case @@ -747,11 +751,12 @@ object Router { // what matters is that the `disable` bit is 0 so that this update doesn't get filtered out ChannelUpdate(signature = ByteVector.empty, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, Platform.currentTime / 1000, messageFlags = 0, channelFlags = 0, extraHop.cltvExpiryDelta, htlcMinimumMsat = 0L, extraHop.feeBaseMsat, extraHop.feeProportionalMillionths, None) - def toFakeUpdates(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Map[ChannelDesc, ChannelUpdate] = { + + def toAssistedChannels(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Map[ShortChannelId, AssistedChannel] = { // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId extraRoute.zip(nextNodeIds).map { - case (extraHop: ExtraHop, nextNodeId) => (ChannelDesc(extraHop.shortChannelId, extraHop.nodeId, nextNodeId) -> toFakeUpdate(extraHop)) + case (extraHop: ExtraHop, nextNodeId) => extraHop.shortChannelId -> AssistedChannel(extraHop, nextNodeId) }.toMap } @@ -762,7 +767,7 @@ object Router { def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2 - def hasChannels(nodeId: PublicKey, channels: Iterable[ChannelData]): Boolean = channels.exists(c => isRelatedTo(c.ann, nodeId)) + def hasChannels(nodeId: PublicKey, channels: Iterable[PublicChannel]): Boolean = channels.exists(c => isRelatedTo(c.ann, nodeId)) def isStale(u: ChannelUpdate): Boolean = { // BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks (1209600 seconds)" @@ -790,12 +795,12 @@ object Router { blockHeight < staleThresholdBlocks && update1_opt.map(isStale).getOrElse(true) && update2_opt.map(isStale).getOrElse(true) } - def getStaleChannels(channels: Iterable[ChannelData]): Iterable[ChannelData] = channels.filter(data => isStale(data.ann, data.update_1_opt, data.update_2_opt)) + def getStaleChannels(channels: Iterable[PublicChannel]): Iterable[PublicChannel] = channels.filter(data => isStale(data.ann, data.update_1_opt, data.update_2_opt)) /** * Filters channels that we want to send to nodes asking for a channel range */ - def keep(firstBlockNum: Long, numberOfBlocks: Long, id: ShortChannelId, channels: Map[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate]): Boolean = { + def keep(firstBlockNum: Long, numberOfBlocks: Long, id: ShortChannelId, channels: Map[ShortChannelId, PublicChannel]): Boolean = { val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } @@ -810,12 +815,14 @@ object Router { /** * This method is used after a payment failed, and we want to exclude some nodes that we know are failing */ - def getIgnoredChannelDesc(updates: Map[ChannelDesc, ChannelUpdate], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = { + def getIgnoredChannelDesc(channels: Map[ShortChannelId, PublicChannel], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = { val desc = if (ignoreNodes.isEmpty) { Iterable.empty[ChannelDesc] } else { // expensive, but node blacklisting shouldn't happen often - updates.keys.filter(desc => ignoreNodes.contains(desc.a) || ignoreNodes.contains(desc.b)) + channels.values + .filter(channelData => ignoreNodes.contains(channelData.ann.nodeId1) || ignoreNodes.contains(channelData.ann.nodeId2)) + .flatMap(channelData => Vector(ChannelDesc(channelData.ann.shortChannelId, channelData.ann.nodeId1, channelData.ann.nodeId2), ChannelDesc(channelData.ann.shortChannelId, channelData.ann.nodeId2, channelData.ann.nodeId1))) } desc } @@ -853,6 +860,7 @@ object Router { numRoutes: Int, extraEdges: Set[GraphEdge] = Set.empty, ignoredEdges: Set[ChannelDesc] = Set.empty, + ignoredVertices: Set[PublicKey] = Set.empty, routeParams: RouteParams): Try[Seq[Hop]] = Try { if (localNodeId == targetNodeId) throw CannotRouteToSelf diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/AboutController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/AboutController.scala index 32671ec67f..0dc25cb5eb 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/AboutController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/AboutController.scala @@ -16,12 +16,11 @@ package fr.acinq.eclair.gui.controllers +import grizzled.slf4j.Logging import javafx.application.HostServices import javafx.fxml.FXML import javafx.scene.text.Text -import grizzled.slf4j.Logging - /** * Created by DPA on 28/09/2016. */ diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala index 22314399cf..7348fb43f3 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala @@ -22,16 +22,16 @@ import fr.acinq.bitcoin.MilliSatoshi import fr.acinq.eclair.CoinUtils import fr.acinq.eclair.channel.{CMD_CLOSE, CMD_FORCECLOSE, Commitments} import fr.acinq.eclair.gui.FxApp +import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction} +import grizzled.slf4j.Logging import javafx.application.Platform import javafx.beans.value.{ChangeListener, ObservableValue} +import javafx.event.{ActionEvent, EventHandler} import javafx.fxml.FXML +import javafx.scene.control.Alert.AlertType import javafx.scene.control._ import javafx.scene.input.{ContextMenuEvent, MouseEvent} import javafx.scene.layout.VBox -import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction} -import grizzled.slf4j.Logging -import javafx.event.{ActionEvent, EventHandler} -import javafx.scene.control.Alert.AlertType /** * Created by DPA on 23/09/2016. diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NodeInfoController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NodeInfoController.scala index 16b8b036d3..6c98a12448 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NodeInfoController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NodeInfoController.scala @@ -16,16 +16,15 @@ package fr.acinq.eclair.gui.controllers +import fr.acinq.eclair.gui.Handlers +import fr.acinq.eclair.gui.utils.{ContextMenuUtils, QRCodeUtils} +import grizzled.slf4j.Logging import javafx.event.ActionEvent import javafx.fxml.FXML import javafx.scene.control._ import javafx.scene.image.ImageView import javafx.stage.Stage -import fr.acinq.eclair.gui.Handlers -import fr.acinq.eclair.gui.utils.{ContextMenuUtils, QRCodeUtils} -import grizzled.slf4j.Logging - import scala.util.{Failure, Success, Try} class NodeInfoController(val address: String, val handlers: Handlers, val stage: Stage) extends Logging { diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NotificationsController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NotificationsController.scala index 0785ca8dbb..6b259846f9 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NotificationsController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/NotificationsController.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.gui.controllers +import fr.acinq.eclair.gui.utils.ContextMenuUtils +import grizzled.slf4j.Logging import javafx.animation._ import javafx.application.Platform import javafx.event.{ActionEvent, EventHandler} @@ -25,9 +27,6 @@ import javafx.scene.image.Image import javafx.scene.layout.{GridPane, VBox} import javafx.util.Duration -import fr.acinq.eclair.gui.utils.ContextMenuUtils -import grizzled.slf4j.Logging - sealed trait NotificationType case object NOTIFICATION_NONE extends NotificationType diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala index 61d598c1c6..33a71d98e1 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala @@ -16,6 +16,12 @@ package fr.acinq.eclair.gui.controllers +import fr.acinq.bitcoin.MilliSatoshi +import fr.acinq.eclair.CoinUtils +import fr.acinq.eclair.gui.utils._ +import fr.acinq.eclair.gui.{FxApp, Handlers} +import fr.acinq.eclair.payment.PaymentRequest +import grizzled.slf4j.Logging import javafx.application.Platform import javafx.event.ActionEvent import javafx.fxml.FXML @@ -24,13 +30,6 @@ import javafx.scene.image.{ImageView, WritableImage} import javafx.scene.layout.GridPane import javafx.stage.Stage -import fr.acinq.bitcoin.MilliSatoshi -import fr.acinq.eclair.CoinUtils -import fr.acinq.eclair.gui.{FxApp, Handlers} -import fr.acinq.eclair.gui.utils._ -import fr.acinq.eclair.payment.PaymentRequest -import grizzled.slf4j.Logging - import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Failure, Success, Try} diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SendPaymentController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SendPaymentController.scala index ef9a77854f..7edd642561 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SendPaymentController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SendPaymentController.scala @@ -16,6 +16,10 @@ package fr.acinq.eclair.gui.controllers +import fr.acinq.eclair.CoinUtils +import fr.acinq.eclair.gui.{FxApp, Handlers} +import fr.acinq.eclair.payment.PaymentRequest +import grizzled.slf4j.Logging import javafx.beans.value.{ChangeListener, ObservableValue} import javafx.event.{ActionEvent, EventHandler} import javafx.fxml.FXML @@ -24,11 +28,6 @@ import javafx.scene.input.KeyCode.{ENTER, TAB} import javafx.scene.input.KeyEvent import javafx.stage.Stage -import fr.acinq.eclair.CoinUtils -import fr.acinq.eclair.gui.{FxApp, Handlers} -import fr.acinq.eclair.payment.PaymentRequest -import grizzled.slf4j.Logging - import scala.util.{Failure, Success, Try} /** diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SplashController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SplashController.scala index b613b5ff67..4a62a091d9 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SplashController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/SplashController.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.gui.controllers +import grizzled.slf4j.Logging import javafx.animation._ import javafx.application.HostServices import javafx.fxml.FXML @@ -24,8 +25,6 @@ import javafx.scene.image.ImageView import javafx.scene.layout.{Pane, VBox} import javafx.util.Duration -import grizzled.slf4j.Logging - /** * Created by DPA on 22/09/2016. */ From 4f3e15f7fd6ecccd95b675034cac15aa46c56d91 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 16:17:30 +0100 Subject: [PATCH 44/86] Use ignoredVertices in route computation, adapt 'makeGraph' to new PublicChannel class --- .../fr/acinq/eclair/DBCompatChecker.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 6 +-- .../fr/acinq/eclair/payment/Autoprobe.scala | 7 ++-- .../scala/fr/acinq/eclair/router/Graph.scala | 41 ++++++++++++++----- .../scala/fr/acinq/eclair/router/Router.scala | 4 +- .../fr/acinq/eclair/router/GraphSpec.scala | 2 +- .../eclair/router/RouteCalculationSpec.scala | 27 +++++++----- 7 files changed, 58 insertions(+), 31 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala index fa05f07708..fd3121bc21 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala @@ -39,7 +39,7 @@ object DBCompatChecker extends Logging { * @param nodeParams */ def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit = - Try(nodeParams.networkDb.listChannels(), nodeParams.networkDb.listNodes(), nodeParams.networkDb.listChannelUpdates()) match { + Try(nodeParams.networkDb.listChannels(), nodeParams.networkDb.listNodes(), nodeParams.networkDb.listChannels()) match { case Success(_) => {} case Failure(_) => throw IncompatibleNetworkDBException } 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 66b9a2f4b1..c8e1397f31 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 @@ -301,7 +301,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor // we won't clean it up, but we won't remember the temporary id on channel termination stay using d.copy(channels = d.channels + (FinalChannelId(channelId) -> channel)) - case Event(RoutingState(channels, updates, nodes), d: ConnectedData) => + case Event(RoutingState(channels, nodes), d: ConnectedData) => // let's send the messages def send(announcements: Iterable[_ <: LightningMessage]) = announcements.foldLeft(0) { case (c, ann) => @@ -310,9 +310,9 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } log.info(s"sending all announcements to {}", remoteNodeId) - val channelsSent = send(channels) + val channelsSent = send(channels.map(_.ann)) val nodesSent = send(nodes) - val updatesSent = send(updates) + val updatesSent = send((channels.map(_.update_1_opt.toSeq) ++ channels.map(_.update_2_opt.toSeq)).flatten) log.info(s"sent all announcements to {}: channels={} updates={} nodes={}", remoteNodeId, channelsSent, updatesSent, nodesSent) stay diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index ced450cedb..c28381fdbd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -20,7 +20,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.ErrorPacket import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure, SendPayment} -import fr.acinq.eclair.router.{Announcements, Data} +import fr.acinq.eclair.router.{Announcements, Data, PublicChannel} import fr.acinq.eclair.wire.UnknownPaymentHash import fr.acinq.eclair.{NodeParams, randomBytes32, secureRandom} @@ -89,9 +89,10 @@ object Autoprobe { def pickPaymentDestination(nodeId: PublicKey, routingData: Data): Option[PublicKey] = { // we only pick direct peers with enabled public channels - val peers = routingData.updates + val peers = routingData.channels .collect { - case (desc, u) if desc.a == nodeId && Announcements.isEnabled(u.channelFlags) && routingData.channels.contains(u.shortChannelId) => desc.b // we only consider outgoing channels that are enabled and announced + case (shortChannelId, c@PublicChannel(ann, _, _, Some(u1), _)) + if c.getNodeIdSameSideAs(u1) == nodeId && Announcements.isEnabled(u1.channelFlags) && routingData.channels.exists(_._1 == shortChannelId) => ann.nodeId2 // we only consider outgoing channels that are enabled and announced } if (peers.isEmpty) { None diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index a9e3189d67..56018fe141 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -75,6 +75,7 @@ object Graph { targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], + ignoredVertices: Set[PublicKey], extraEdges: Set[GraphEdge], pathsToFind: Int, wr: Option[WeightRatios], @@ -89,7 +90,7 @@ object Graph { val candidates = new mutable.PriorityQueue[WeightedPath] // find the shortest path, k = 0 - val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, amountMsat, ignoredEdges, extraEdges, RichWeight(amountMsat, 0, 0, 0), boundaries, currentBlockHeight, wr) + val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, amountMsat, ignoredEdges, ignoredVertices, extraEdges, RichWeight(amountMsat, 0, 0, 0), boundaries, currentBlockHeight, wr) shortestPaths += WeightedPath(shortestPath, pathWeight(shortestPath, amountMsat, isPartial = false, currentBlockHeight, wr)) // avoid returning a list with an empty path @@ -125,7 +126,7 @@ object Graph { val returningEdges = rootPathEdges.lastOption.map(last => graph.getEdgesBetween(last.desc.b, last.desc.a)).toSeq.flatten.map(_.desc) // find the "spur" path, a sub-path going from the spur edge to the target avoiding previously found sub-paths - val spurPath = dijkstraShortestPath(graph, spurEdge.desc.a, targetNode, amountMsat, ignoredEdges ++ edgesToIgnore.toSet ++ returningEdges.toSet, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr) + val spurPath = dijkstraShortestPath(graph, spurEdge.desc.a, targetNode, amountMsat, ignoredEdges ++ edgesToIgnore.toSet ++ returningEdges.toSet, ignoredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr) // if there wasn't a path the spur will be empty if (spurPath.nonEmpty) { @@ -180,6 +181,7 @@ object Graph { targetNode: PublicKey, amountMsat: Long, ignoredEdges: Set[ChannelDesc], + ignoredVertices: Set[PublicKey], extraEdges: Set[GraphEdge], initialWeight: RichWeight, boundaries: RichWeight => Boolean, @@ -234,7 +236,7 @@ object Graph { if (edge.update.htlcMaximumMsat.forall(newMinimumKnownWeight.cost + amountMsat <= _) && newMinimumKnownWeight.cost + amountMsat >= edge.update.htlcMinimumMsat && boundaries(newMinimumKnownWeight) && // check if this neighbor edge would break off the 'boundaries' - !ignoredEdges.contains(edge.desc) + !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor) ) { // we call containsKey first because "getOrDefault" is not available in JDK7 @@ -527,7 +529,7 @@ object Graph { def apply(edge: GraphEdge): DirectedGraph = new DirectedGraph(Map()).addEdge(edge.desc, edge.update) def apply(edges: Seq[GraphEdge]): DirectedGraph = { - makeGraph(edges.map(e => e.desc -> e.update).toMap) + DirectedGraph().addEdges(edges.map(e => (e.desc, e.update))) } // optimized constructor @@ -535,16 +537,33 @@ object Graph { // initialize the map with the appropriate size to avoid resizing during the graph initialization val mutableMap = new {} with mutable.HashMap[PublicKey, List[GraphEdge]] { - override def initialSize: Int = descAndUpdates.size + 1 + override def initialSize: Int = channels.size + 1 } // add all the vertices and edges in one go - descAndUpdates.foreach { case (desc, update) => - // create or update vertex (desc.b) and update its neighbor - mutableMap.put(desc.b, GraphEdge(desc, update) +: mutableMap.getOrElse(desc.b, List.empty[GraphEdge])) - mutableMap.get(desc.a) match { - case None => mutableMap += desc.a -> List.empty[GraphEdge] - case _ => + channels.values.foreach { channel => + + // make desc for both directions + val (desc1, desc2) = ( + channel.update_1_opt.map(u1 => Router.getDesc(u1, channel.ann)), + channel.update_2_opt.map(u2 => Router.getDesc(u2, channel.ann)) + ) + + desc1.map { descAB => + mutableMap.put(descAB.b, GraphEdge(descAB, channel.update_1_opt.get) +: mutableMap.getOrElse(descAB.b, List.empty[GraphEdge])) + mutableMap.get(descAB.a) match { + case None => mutableMap += descAB.a -> List.empty[GraphEdge] + case _ => + } + } + + desc2.map { descBA => + mutableMap.put(descBA.b, GraphEdge(descBA, channel.update_2_opt.get) +: mutableMap.getOrElse(descBA.b, List.empty[GraphEdge])) + mutableMap.get(descBA.a) match { + case None => mutableMap += descBA.a -> List.empty[GraphEdge] + case _ => + } + } } 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 3865a29d8c..dc17260e95 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 @@ -873,9 +873,9 @@ object Router { weight.cltv <= routeParams.routeMaxCltv } - val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList match { + val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList match { case Nil if routeParams.routeMaxLength < ROUTE_MAX_LENGTH => // if not found within the constraints we relax and repeat the search - return findRoute(g, localNodeId, targetNodeId, amountMsat, numRoutes, extraEdges, ignoredEdges, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV)) + return findRoute(g, localNodeId, targetNodeId, amountMsat, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV)) case Nil => throw RouteNotFound case routes => routes.find(_.path.size == 1) match { case Some(directRoute) => directRoute :: Nil diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index 561860fb04..9166e98bd8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -54,7 +54,7 @@ class GraphSpec extends FunSuite { makeUpdate(6L, b, e, 0, 0) ) - DirectedGraph.makeGraph(updates.toMap) + DirectedGraph().addEdges(updates) } test("instantiate a graph, with vertices and then add edges") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 3b270ebc68..6f0ee155bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} @@ -385,7 +385,7 @@ class RouteCalculationSpec extends FunSuite { val extraHops = extraHop1 :: extraHop2 :: extraHop3 :: extraHop4 :: Nil - val fakeUpdates = Router.toFakeUpdates(extraHops, e) + val fakeUpdates = Router.toAssistedChannels(extraHops, e) assert(fakeUpdates == Map( ChannelDesc(extraHop1.shortChannelId, a, b) -> Router.toFakeUpdate(extraHop1), @@ -480,8 +480,8 @@ class RouteCalculationSpec extends FunSuite { ShortChannelId(6L) -> makeChannel(6L, f, h), ShortChannelId(7L) -> makeChannel(7L, h, i), ShortChannelId(8L) -> makeChannel(8L, i, j) - ) + val updates = List( makeUpdate(1L, a, b, 10, 10), makeUpdate(2L, b, c, 10, 10), @@ -494,7 +494,14 @@ class RouteCalculationSpec extends FunSuite { makeUpdate(8L, i, j, 10, 10) ).toMap - val ignored = Router.getIgnoredChannelDesc(updates, ignoreNodes = Set(c, j, randomKey.publicKey)) + val publicChannels = channels.map { case (shortChannelId, announcement) => + val (_, update) = updates.find{ case (d, u) => d.shortChannelId == shortChannelId}.get + val pc = PublicChannel(announcement, ByteVector32.Zeroes, Satoshi(1000), update_1_opt = Some(update), update_2_opt = None) + (shortChannelId, pc) + } + + + val ignored = Router.getIgnoredChannelDesc(publicChannels, ignoreNodes = Set(c, j, randomKey.publicKey)) assert(ignored.toSet === Set( ChannelDesc(ShortChannelId(2L), b, c), @@ -624,11 +631,11 @@ class RouteCalculationSpec extends FunSuite { makeUpdate(5L, e, f, 1, 0), makeUpdate(6L, b, c, 1, 0), makeUpdate(7L, c, f, 1, 0) - ).toMap + ) - val graph = DirectedGraph.makeGraph(edges) + val graph = DirectedGraph().addEdges(edges) - val fourShortestPaths = Graph.yenKshortestPaths(graph, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 4, None, 0, noopBoundaries) + val fourShortestPaths = Graph.yenKshortestPaths(graph, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, None, 0, noopBoundaries) assert(fourShortestPaths.size === 4) assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F @@ -662,7 +669,7 @@ class RouteCalculationSpec extends FunSuite { val graph = DirectedGraph().addEdges(edges) - val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 2, None, 0, noopBoundaries) + val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, None, 0, noopBoundaries) assert(twoShortestPaths.size === 2) val shortest = twoShortestPaths(0) @@ -696,7 +703,7 @@ class RouteCalculationSpec extends FunSuite { val graph = DirectedGraph().addEdges(edges) //we ask for 3 shortest paths but only 2 can be found - val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 3, None, 0, noopBoundaries) + val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, None, 0, noopBoundaries) assert(foundPaths.size === 2) assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) === 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F @@ -876,7 +883,7 @@ object RouteCalculationSpec { htlcMaximumMsat = maxHtlcMsat ) - def makeGraph(updates: Map[ChannelDesc, ChannelUpdate]) = DirectedGraph.makeGraph(updates) + def makeGraph(updates: Map[ChannelDesc, ChannelUpdate]) = DirectedGraph().addEdges(updates.toSeq) def hops2Ids(route: Seq[Hop]) = route.map(hop => hop.lastUpdate.shortChannelId.toLong) From 01c9b38d40eb57331494b6c170ceee8304199b3a Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 15 Mar 2019 16:58:23 +0100 Subject: [PATCH 45/86] Fix test in route calculation spec --- .../eclair/db/sqlite/SqliteNetworkDb.scala | 19 +++++++++------ .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../eclair/router/RouteCalculationSpec.scala | 24 +++++++++---------- .../fr/acinq/eclair/router/RouterSpec.scala | 2 +- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index 95cb73f164..a8e5a4d60f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -21,10 +21,12 @@ import java.sql.Connection import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.db.NetworkDb -import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.router.{Announcements, PublicChannel} import fr.acinq.eclair.wire.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} -import scodec.bits.BitVector +import scodec.bits.{BitVector, ByteVector} + +import scala.collection.immutable.SortedMap class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { @@ -99,13 +101,16 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { shortChannelIds.grouped(1000).foreach(removeChannelsInternal) } - override def listChannels(): Map[ChannelAnnouncement, (ByteVector32, Satoshi)] = { + override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT data, txid, capacity_sat FROM channels") - var m: Map[ChannelAnnouncement, (ByteVector32, Satoshi)] = Map() + var m = SortedMap.empty[ShortChannelId, PublicChannel] while (rs.next()) { - m += (channelAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value -> - (ByteVector32.fromValidHex(rs.getString("txid")), Satoshi(rs.getLong("capacity_sat")))) + val ann = channelAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value + val txId = ByteVector32.fromValidHex(rs.getString("txid")) + val capacity = rs.getLong("capacity_sat") + + m = m + (ann.shortChannelId -> PublicChannel(ann, txId, Satoshi(capacity), None, None)) } m } @@ -129,7 +134,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { } } - override def listChannelUpdates(): Seq[ChannelUpdate] = { + def listChannelUpdates(): Seq[ChannelUpdate] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT data FROM channel_updates") codecSequence(rs, channelUpdateCodec) 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 c8e1397f31..c0bd200f77 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 @@ -312,7 +312,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor log.info(s"sending all announcements to {}", remoteNodeId) val channelsSent = send(channels.map(_.ann)) val nodesSent = send(nodes) - val updatesSent = send((channels.map(_.update_1_opt.toSeq) ++ channels.map(_.update_2_opt.toSeq)).flatten) + val updatesSent = send(channels.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq)) log.info(s"sent all announcements to {}: channels={} updates={} nodes={}", remoteNodeId, channelsSent, updatesSent, nodesSent) stay diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 6f0ee155bb..33c1da5835 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -385,13 +385,15 @@ class RouteCalculationSpec extends FunSuite { val extraHops = extraHop1 :: extraHop2 :: extraHop3 :: extraHop4 :: Nil - val fakeUpdates = Router.toAssistedChannels(extraHops, e) + val fakeUpdates: Map[ShortChannelId, ExtraHop] = Router.toAssistedChannels(extraHops, e).map { case (shortChannelId, assistedChannel) => + (shortChannelId, assistedChannel.extraHop) + } assert(fakeUpdates == Map( - ChannelDesc(extraHop1.shortChannelId, a, b) -> Router.toFakeUpdate(extraHop1), - ChannelDesc(extraHop2.shortChannelId, b, c) -> Router.toFakeUpdate(extraHop2), - ChannelDesc(extraHop3.shortChannelId, c, d) -> Router.toFakeUpdate(extraHop3), - ChannelDesc(extraHop4.shortChannelId, d, e) -> Router.toFakeUpdate(extraHop4) + extraHop1.shortChannelId -> extraHop1, + extraHop2.shortChannelId -> extraHop2, + extraHop3.shortChannelId -> extraHop3, + extraHop4.shortChannelId -> extraHop4 )) } @@ -496,19 +498,17 @@ class RouteCalculationSpec extends FunSuite { val publicChannels = channels.map { case (shortChannelId, announcement) => val (_, update) = updates.find{ case (d, u) => d.shortChannelId == shortChannelId}.get - val pc = PublicChannel(announcement, ByteVector32.Zeroes, Satoshi(1000), update_1_opt = Some(update), update_2_opt = None) + val pc = PublicChannel(announcement, ByteVector32.Zeroes, Satoshi(1000), update_1_opt = Some(update), update_2_opt = Some(update)) (shortChannelId, pc) } val ignored = Router.getIgnoredChannelDesc(publicChannels, ignoreNodes = Set(c, j, randomKey.publicKey)) - assert(ignored.toSet === Set( - ChannelDesc(ShortChannelId(2L), b, c), - ChannelDesc(ShortChannelId(2L), c, b), - ChannelDesc(ShortChannelId(3L), c, d), - ChannelDesc(ShortChannelId(8L), i, j) - )) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(2L), b, c))) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(2L), c, b))) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(3L), c, d))) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(8L), i, j))) } test("limit routes to 20 hops") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 7cafbf9fe7..e4a374b344 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -234,7 +234,7 @@ class RouterSpec extends BaseRouterSpec { val state = sender.expectMsgType[RoutingState] assert(state.channels.size == 4) assert(state.nodes.size == 6) - assert(state.updates.size == 8) + assert(state.channels.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 8) } test("ask for channels that we marked as stale for which we receive a new update") { fixture => From 0031f3b8a0519f82b30e7e80dbd8bb0d20b2e876 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 15 Mar 2019 18:55:58 +0100 Subject: [PATCH 46/86] reworked network db --- .../scala/fr/acinq/eclair/db/NetworkDb.scala | 8 +- .../eclair/db/sqlite/SqliteNetworkDb.scala | 90 +++++++++--------- .../acinq/eclair/db/sqlite/SqliteUtils.scala | 8 ++ .../scala/fr/acinq/eclair/router/Router.scala | 4 +- .../eclair/wire/LightningMessageTypes.scala | 3 + .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 91 +++++++++++++------ 6 files changed, 120 insertions(+), 84 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index 8f489c5124..3839a42e93 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -36,13 +36,11 @@ trait NetworkDb { def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi) - def removeChannel(shortChannelId: ShortChannelId) = removeChannels(Seq(shortChannelId)) + def updateChannel(u: ChannelUpdate) - def removeChannels(shortChannelIds: Iterable[ShortChannelId]) - - def addChannelUpdate(u: ChannelUpdate) + def removeChannel(shortChannelId: ShortChannelId) = removeChannels(Set(shortChannelId)) - def updateChannelUpdate(u: ChannelUpdate) + def removeChannels(shortChannelIds: Iterable[ShortChannelId]) def listChannels(): SortedMap[ShortChannelId, PublicChannel] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index a8e5a4d60f..d414d2e697 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -21,27 +21,39 @@ import java.sql.Connection import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.db.NetworkDb -import fr.acinq.eclair.router.{Announcements, PublicChannel} +import fr.acinq.eclair.router.PublicChannel import fr.acinq.eclair.wire.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} -import scodec.bits.{BitVector, ByteVector} +import grizzled.slf4j.Logging import scala.collection.immutable.SortedMap -class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { +class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { import SqliteUtils._ + import SqliteUtils.ExtendedResultSet._ val DB_NAME = "network" - val CURRENT_VERSION = 1 + val CURRENT_VERSION = 2 using(sqlite.createStatement()) { statement => - require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed - statement.execute("PRAGMA foreign_keys = ON") + getVersion(statement, DB_NAME, CURRENT_VERSION) match { + case 1 => + // channel_update are cheap to retrieve, so let's just wipe them out and they'll get resynced + statement.execute("PRAGMA foreign_keys = ON") + logger.warn("migrating network db version 1->2") + statement.executeUpdate("ALTER TABLE channels RENAME COLUMN data TO channel_announcement") + statement.executeUpdate("ALTER TABLE channels ADD COLUMN channel_update_1 BLOB NULL") + statement.executeUpdate("ALTER TABLE channels ADD COLUMN channel_update_2 BLOB NULL") + statement.executeUpdate("DROP TABLE channel_updates") + statement.execute("PRAGMA foreign_keys = OFF") + setVersion(statement, DB_NAME, CURRENT_VERSION) + logger.warn("migration complete") + case 2 => () // nothing to do + case unknown => throw new IllegalArgumentException(s"unknown version $unknown for network db") + } statement.executeUpdate("CREATE TABLE IF NOT EXISTS nodes (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid STRING NOT NULL, data BLOB NOT NULL, capacity_sat INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_updates (short_channel_id INTEGER NOT NULL, node_flag INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(short_channel_id, node_flag), FOREIGN KEY(short_channel_id) REFERENCES channels(short_channel_id))") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_updates_idx ON channel_updates(short_channel_id)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid STRING NOT NULL, channel_announcement BLOB NOT NULL, capacity_sat INTEGER NOT NULL, channel_update_1 BLOB NULL, channel_update_2 BLOB NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS pruned (short_channel_id INTEGER NOT NULL PRIMARY KEY)") } @@ -76,7 +88,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { } override def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?, ?)")) { statement => + using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?, ?, NULL, NULL)")) { statement => statement.setLong(1, c.shortChannelId.toLong) statement.setString(2, txid.toHex) statement.setBytes(3, channelAnnouncementCodec.encode(c).require.toByteArray) @@ -85,59 +97,39 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { } } - override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = { - - def removeChannelsInternal(shortChannelIds: Iterable[ShortChannelId]): Unit = { - val ids = shortChannelIds.map(_.toLong).mkString(",") - using(sqlite.createStatement) { statement => - statement.execute("BEGIN TRANSACTION") - statement.executeUpdate(s"DELETE FROM channel_updates WHERE short_channel_id IN ($ids)") - statement.executeUpdate(s"DELETE FROM channels WHERE short_channel_id IN ($ids)") - statement.execute("COMMIT TRANSACTION") - } + override def updateChannel(u: ChannelUpdate): Unit = { + val column = if (u.isNode1) "channel_update_1" else "channel_update_2" + using(sqlite.prepareStatement(s"UPDATE channels SET $column=? WHERE short_channel_id=?")) { statement => + statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray) + statement.setLong(2, u.shortChannelId.toLong) + statement.executeUpdate() } - - // remove channels by batch of 1000 - shortChannelIds.grouped(1000).foreach(removeChannelsInternal) } override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT data, txid, capacity_sat FROM channels") + val rs = statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM channels") var m = SortedMap.empty[ShortChannelId, PublicChannel] while (rs.next()) { - val ann = channelAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value + val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") - - m = m + (ann.shortChannelId -> PublicChannel(ann, txId, Satoshi(capacity), None, None)) + val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) + val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) + m = m + (ann.shortChannelId -> PublicChannel(ann, txId, Satoshi(capacity), channel_update_1_opt, channel_update_2_opt)) } m } } - override def addChannelUpdate(u: ChannelUpdate): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO channel_updates VALUES (?, ?, ?)")) { statement => - statement.setLong(1, u.shortChannelId.toLong) - statement.setBoolean(2, Announcements.isNode1(u.channelFlags)) - statement.setBytes(3, channelUpdateCodec.encode(u).require.toByteArray) - statement.executeUpdate() - } - } - - override def updateChannelUpdate(u: ChannelUpdate): Unit = { - using(sqlite.prepareStatement("UPDATE channel_updates SET data=? WHERE short_channel_id=? AND node_flag=?")) { statement => - statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray) - statement.setLong(2, u.shortChannelId.toLong) - statement.setBoolean(3, Announcements.isNode1(u.channelFlags)) - statement.executeUpdate() - } - } - - def listChannelUpdates(): Seq[ChannelUpdate] = { - using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT data FROM channel_updates") - codecSequence(rs, channelUpdateCodec) + override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = { + using(sqlite.createStatement) { statement => + shortChannelIds + .grouped(1000) // remove channels by batch of 1000 + .foreach {group => + val ids = shortChannelIds.map(_.toLong).mkString(",") + statement.executeUpdate(s"DELETE FROM channels WHERE short_channel_id IN ($ids)") + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 8d8e602e60..5e7a995fb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -60,6 +60,12 @@ object SqliteUtils { res.getInt("version") } + def setVersion(statement: Statement, db_name: String, currentVersion: Int) = { + statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") + // if there was no version for the current db, then insert the current version + statement.executeUpdate(s"INSERT OR IGNORE INTO versions VALUES ('$db_name', $currentVersion)") + } + /** * This helper assumes that there is a "data" column available, decodable with the provided codec * @@ -96,6 +102,8 @@ object SqliteUtils { case class ExtendedResultSet(rs: ResultSet) { + def getBitVectorOpt(columnLabel: String): Option[BitVector] = Option(rs.getBytes(columnLabel)).map(BitVector(_)) + def getByteVector(columnLabel: String): ByteVector = ByteVector(rs.getBytes(columnLabel)) def getByteVector32(columnLabel: String): ByteVector32 = ByteVector32(ByteVector(rs.getBytes(columnLabel))) 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 dc17260e95..a9dbf206aa 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 @@ -650,7 +650,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } else if (pc.getSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannelUpdate(u) + db.updateChannel(u) // update the graph val graph1 = Announcements.isEnabled(u.channelFlags) match { case true => d.graph.removeEdge(desc).addEdge(desc, u) @@ -660,7 +660,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.addChannelUpdate(u) + db.updateChannel(u) // we also need to update the graph val graph1 = d.graph.addEdge(desc, u) d.copy(channels = d.channels + (u.shortChannelId -> pc.updateSameSideAs(u)), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) 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 880ab0d05d..9b3d9b90ae 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 @@ -21,6 +21,7 @@ import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import com.google.common.base.Charsets import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar} +import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -221,6 +222,8 @@ case class ChannelUpdate(signature: ByteVector, feeProportionalMillionths: Long, htlcMaximumMsat: Option[Long]) extends RoutingMessage with HasTimestamp with HasChainHash { require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") + + def isNode1 = Announcements.isNode1(channelFlags) } case class PerHopPayload(shortChannelId: ShortChannelId, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 76a509308e..1a15416474 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -16,15 +16,18 @@ package fr.acinq.eclair.db -import java.sql.DriverManager +import java.sql.{Connection, DriverManager} +import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb -import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.db.sqlite.SqliteUtils._ +import fr.acinq.eclair.router.{Announcements, PublicChannel} import fr.acinq.eclair.wire.{Color, NodeAddress, Tor2} import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomKey} import org.scalatest.FunSuite -import org.sqlite.SQLiteException + +import scala.collection.SortedMap class SqliteNetworkDbSpec extends FunSuite { @@ -39,6 +42,19 @@ class SqliteNetworkDbSpec extends FunSuite { val db2 = new SqliteNetworkDb(sqlite) } + test("migration test 1->2") { + val sqlite = inmem + using(sqlite.createStatement()) { statement => + statement.execute("PRAGMA foreign_keys = ON") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS nodes (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid STRING NOT NULL, data BLOB NOT NULL, capacity_sat INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_updates (short_channel_id INTEGER NOT NULL, node_flag INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(short_channel_id, node_flag), FOREIGN KEY(short_channel_id) REFERENCES channels(short_channel_id))") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_updates_idx ON channel_updates(short_channel_id)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS pruned (short_channel_id INTEGER NOT NULL PRIMARY KEY)") + } + simpleTest(sqlite) + } + test("add/remove/list nodes") { val sqlite = inmem val db = new SqliteNetworkDb(sqlite) @@ -63,15 +79,26 @@ class SqliteNetworkDbSpec extends FunSuite { assert(node_4.addresses == List(Tor2("aaaqeayeaudaocaj", 42000))) } - test("add/remove/list channels and channel_updates") { + def simpleTest(sqlite: Connection) = { val sqlite = inmem val db = new SqliteNetworkDb(sqlite) def sig = Crypto.encodeSignature(Crypto.sign(randomKey.toBin, randomKey)) :+ 1.toByte - val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) - val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(43), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) - val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(44), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + def generatePubkeyHigherThan(priv: PrivateKey) = { + var res = priv + while(!Announcements.isNode1(priv.publicKey, res.publicKey)) res = randomKey + res + } + + // in order to differentiate channel_updates 1/2 we order public keys + val a = randomKey + val b = generatePubkeyHigherThan(a) + val c = generatePubkeyHigherThan(b) + + val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), a.publicKey, b.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(43), a.publicKey, c.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(44), b.publicKey, c.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) val txid_1 = randomBytes32 val txid_2 = randomBytes32 @@ -84,24 +111,34 @@ class SqliteNetworkDbSpec extends FunSuite { assert(db.listChannels().size === 1) db.addChannel(channel_2, txid_2, capacity) db.addChannel(channel_3, txid_3, capacity) - assert(db.listChannels().toSet === Set((channel_1, (txid_1, capacity)), (channel_2, (txid_2, capacity)), (channel_3, (txid_3, capacity)))) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, None, None), + channel_2.shortChannelId -> PublicChannel(channel_2, txid_2, capacity, None, None), + channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, None, None))) db.removeChannel(channel_2.shortChannelId) - assert(db.listChannels().toSet === Set((channel_1, (txid_1, capacity)), (channel_3, (txid_3, capacity)))) - - val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(42), 5, 7000000, 50000, 100, 500000000L, true) - val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(43), 5, 7000000, 50000, 100, 500000000L, true) - val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(44), 5, 7000000, 50000, 100, 500000000L, true) - - assert(db.listChannelUpdates().toSet === Set.empty) - db.addChannelUpdate(channel_update_1) - db.addChannelUpdate(channel_update_1) // duplicate is ignored - assert(db.listChannelUpdates().size === 1) - intercept[SQLiteException](db.addChannelUpdate(channel_update_2)) - db.addChannelUpdate(channel_update_3) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, None, None), + channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, None, None))) + + val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, a, b.publicKey, ShortChannelId(42), 5, 7000000, 50000, 100, 500000000L, true) + val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, a.publicKey, ShortChannelId(42), 5, 7000000, 50000, 100, 500000000L, true) + val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, c.publicKey, ShortChannelId(44), 5, 7000000, 50000, 100, 500000000L, true) + + db.updateChannel(channel_update_1) + db.updateChannel(channel_update_1) // duplicate is ignored + db.updateChannel(channel_update_2) + db.updateChannel(channel_update_3) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, Some(channel_update_1), Some(channel_update_2)), + channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, Some(channel_update_3), None))) db.removeChannel(channel_3.shortChannelId) - assert(db.listChannels().toSet === Set((channel_1, (txid_1, capacity)))) - assert(db.listChannelUpdates().toSet === Set(channel_update_1)) - db.updateChannelUpdate(channel_update_1) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, Some(channel_update_1), Some(channel_update_2)))) + } + + test("add/remove/list channels and channel_updates") { + val sqlite = inmem + simpleTest(sqlite) } test("remove many channels") { @@ -117,14 +154,12 @@ class SqliteNetworkDbSpec extends FunSuite { val updates = shortChannelIds.map(id => template.copy(shortChannelId = id)) val txid = randomBytes32 channels.foreach(ca => db.addChannel(ca, txid, capacity)) - updates.foreach(u => db.addChannelUpdate(u)) - assert(db.listChannels().keySet === channels.toSet) - assert(db.listChannelUpdates() === updates) + updates.foreach(u => db.updateChannel(u)) + assert(db.listChannels().keySet === channels.map(_.shortChannelId).toSet) val toDelete = channels.map(_.shortChannelId).drop(500).take(2500) db.removeChannels(toDelete) - assert(db.listChannels().keySet === channels.filterNot(a => toDelete.contains(a.shortChannelId)).toSet) - assert(db.listChannelUpdates().toSet === updates.filterNot(u => toDelete.contains(u.shortChannelId)).toSet) + assert(db.listChannels().keySet === (channels.map(_.shortChannelId).toSet -- toDelete)) } test("prune many channels") { From bf54d36782b167611fcad38c7b6b16bfb4438e62 Mon Sep 17 00:00:00 2001 From: pm47 Date: Sun, 17 Mar 2019 20:05:49 +0100 Subject: [PATCH 47/86] fixed some tests --- .../scala/fr/acinq/eclair/router/Router.scala | 21 ++++++++++++------- .../eclair/integration/IntegrationSpec.scala | 14 ++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) 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 a9dbf206aa..6b50bb9917 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 @@ -62,6 +62,13 @@ case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, ca def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) ann.nodeId1 else ann.nodeId2 def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt def updateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) + def updateFor(n: PublicKey): Option[ChannelUpdate] = { + if (n == ann.nodeId1) { + update_1_opt + } else if (n == ann.nodeId2) { + update_2_opt + } else throw new IllegalArgumentException("this node is unrelated to this channel") + } } case class PrivateChannel(nodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate])(implicit nodeParams: NodeParams) { val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeParams.nodeId, nodeId)) (nodeParams.nodeId, nodeId) else (nodeId, nodeParams.nodeId) @@ -409,16 +416,15 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom sender ! d.channels.values.map(_.ann) stay + case Event('channelsMap, d) => + sender ! d.channels + stay + case Event('updates, d) => val updates: Iterable[ChannelUpdate] = d.channels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt) ++ d.privateChannels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt) sender ! updates stay -// case Event('updatesMap, d) => -// val updatesMap -// sender ! (d.updates ++ d.privateUpdates) -// stay - case Event('data, d) => sender ! d stay @@ -427,15 +433,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // we convert extra routing info provided in the payment request to fake channel_update // it takes precedence over all other channel_updates we know val assistedChannels: Map[ShortChannelId, AssistedChannel] = assistedRoutes.flatMap(toAssistedChannels(_, end)).toMap - // we also filter out updates corresponding to channels/nodes that are blacklisted for this particular request - // TODO: in case of duplicates, d.updates will be overridden by assistedUpdates even if they are more recent! val extraEdges = assistedChannels.values.map(ac => GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop))).toSet + val ignoredEdges = ignoreChannels ++ d.excludedChannels val params = params_opt.getOrElse(defaultRouteParams) val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), ignoreNodes.map(_.toBin).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) log.info(s"finding a route with randomize={} params={}", routesToFind > 1, params) - findRoute(d.graph, start, end, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoreChannels, ignoredVertices = ignoreNodes, routeParams = params) + findRoute(d.graph, start, end, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredEdges, ignoredVertices = ignoreNodes, routeParams = params) .map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels)) .recover { case t => sender ! Status.Failure(t) } stay 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 8c4a36b3e6..693beceb4c 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 @@ -38,11 +38,11 @@ import fr.acinq.eclair.payment.PaymentLifecycle.{State => _, _} import fr.acinq.eclair.payment.{LocalPaymentHandler, PaymentRequest} import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.ROUTE_MAX_LENGTH -import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, ChannelDesc, RouteParams} +import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, ChannelDesc, PublicChannel, RouteParams} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, Kit, Setup, randomBytes32} +import fr.acinq.eclair.{Globals, Kit, Setup, ShortChannelId, randomBytes32} import grizzled.slf4j.Logging import org.json4s.JsonAST.JValue import org.json4s.{DefaultFormats, JString} @@ -287,10 +287,10 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService awaitCond({ // in the meantime, the router will have updated its state - sender.send(nodes("A").router, 'updatesMap) + sender.send(nodes("A").router, 'channelsMap) // we then put everything back like before by asking B to refresh its channel update (this will override the one we created) - val update = sender.expectMsgType[Map[ChannelDesc, ChannelUpdate]](10 seconds).apply(ChannelDesc(channelUpdateBC.shortChannelId, nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)) - update == channelUpdateBC + val u_opt = sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId).updateFor(nodes("B").nodeParams.nodeId) + u_opt == Some(channelUpdateBC) }, max = 30 seconds, interval = 1 seconds) // first let's wait 3 seconds to make sure the timestamp of the new channel_update will be strictly greater than the former @@ -303,8 +303,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(channelUpdateBC_new.timestamp > channelUpdateBC.timestamp) assert(channelUpdateBC_new.cltvExpiryDelta == nodes("B").nodeParams.expiryDeltaBlocks) awaitCond({ - sender.send(nodes("A").router, 'updatesMap) - val u = sender.expectMsgType[Map[ChannelDesc, ChannelUpdate]].apply(ChannelDesc(channelUpdateBC.shortChannelId, nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)) + sender.send(nodes("A").router, 'channelsMap) + val u = sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId).updateFor(nodes("B").nodeParams.nodeId).get u.cltvExpiryDelta == nodes("B").nodeParams.expiryDeltaBlocks }, max = 30 seconds, interval = 1 second) } From 6d8d0522f5f290370ae869a7eee8c7217eb72d27 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 18 Mar 2019 10:45:48 +0100 Subject: [PATCH 48/86] removed duplicate call --- .../src/main/scala/fr/acinq/eclair/DBCompatChecker.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala index fd3121bc21..2b6e99e116 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala @@ -39,7 +39,7 @@ object DBCompatChecker extends Logging { * @param nodeParams */ def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit = - Try(nodeParams.networkDb.listChannels(), nodeParams.networkDb.listNodes(), nodeParams.networkDb.listChannels()) match { + Try(nodeParams.networkDb.listChannels(), nodeParams.networkDb.listNodes()) match { case Success(_) => {} case Failure(_) => throw IncompatibleNetworkDBException } From 1544f436b0084f095b9893929a0052e9dfb9558d Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 18 Mar 2019 10:50:00 +0100 Subject: [PATCH 49/86] re-enabled events --- eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6b50bb9917..1755b90e72 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 @@ -171,7 +171,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom val initNodes = nodes.map(n => (n.nodeId -> n)).toMap // send events for remaining channels/nodes context.system.eventStream.publish(ChannelsDiscovered(initChannels.values.map(pc => SingleChannelDiscovered(pc.ann, pc.capacity)))) - //context.system.eventStream.publish(ChannelUpdatesReceived(initChannelUpdates.values)) + context.system.eventStream.publish(ChannelUpdatesReceived(initChannels.values.flatMap(pc => pc.update_1_opt ++ pc.update_2_opt ++ Nil))) context.system.eventStream.publish(NodesDiscovered(initNodes.values)) // watch the funding tx of all these channels From ee1f3a54ed1a3561134919125c32c9c5bf4171ee Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 18 Mar 2019 10:51:42 +0100 Subject: [PATCH 50/86] simplified helper function declaration --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 1755b90e72..484264e0ab 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 @@ -62,13 +62,7 @@ case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, ca def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) ann.nodeId1 else ann.nodeId2 def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt def updateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) - def updateFor(n: PublicKey): Option[ChannelUpdate] = { - if (n == ann.nodeId1) { - update_1_opt - } else if (n == ann.nodeId2) { - update_2_opt - } else throw new IllegalArgumentException("this node is unrelated to this channel") - } + def updateFor(n: PublicKey): Option[ChannelUpdate] = if (n == ann.nodeId1) update_1_opt else if (n == ann.nodeId2) update_2_opt else throw new IllegalArgumentException("this node is unrelated to this channel") } case class PrivateChannel(nodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate])(implicit nodeParams: NodeParams) { val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeParams.nodeId, nodeId)) (nodeParams.nodeId, nodeId) else (nodeId, nodeParams.nodeId) From eeb5886d3c8dfae4f45dff97cb730bbebea5876e Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 18 Mar 2019 15:59:58 +0100 Subject: [PATCH 51/86] fixed migration and updated sqlite --- eclair-core/pom.xml | 2 +- .../acinq/eclair/db/sqlite/SqliteUtils.scala | 2 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 27 ++++++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 679f4a5915..78cd3cc2d2 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -203,7 +203,7 @@ org.xerial sqlite-jdbc - 3.21.0.1 + 3.27.2 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 5e7a995fb6..65e7b2dd3c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -63,7 +63,7 @@ object SqliteUtils { def setVersion(statement: Statement, db_name: String, currentVersion: Int) = { statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") // if there was no version for the current db, then insert the current version - statement.executeUpdate(s"INSERT OR IGNORE INTO versions VALUES ('$db_name', $currentVersion)") + statement.executeUpdate(s"UPDATE versions SET version=$currentVersion WHERE db_name='$db_name'") } /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 1a15416474..8930a53136 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -44,7 +44,9 @@ class SqliteNetworkDbSpec extends FunSuite { test("migration test 1->2") { val sqlite = inmem + using(sqlite.createStatement()) { statement => + getVersion(statement, "network", 1) // this will set version to 1 statement.execute("PRAGMA foreign_keys = ON") statement.executeUpdate("CREATE TABLE IF NOT EXISTS nodes (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid STRING NOT NULL, data BLOB NOT NULL, capacity_sat INTEGER NOT NULL)") @@ -52,7 +54,31 @@ class SqliteNetworkDbSpec extends FunSuite { statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_updates_idx ON channel_updates(short_channel_id)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS pruned (short_channel_id INTEGER NOT NULL PRIMARY KEY)") } + + + using(sqlite.createStatement()) { statement => + assert(getVersion(statement, "network", 2) == 1) + } + + // first round: this will trigger a migration simpleTest(sqlite) + + using(sqlite.createStatement()) { statement => + assert(getVersion(statement, "network", 2) == 2) + } + + using(sqlite.createStatement()) { statement => + statement.executeUpdate("DELETE FROM nodes") + statement.executeUpdate("DELETE FROM channels") + } + + // second round: no migration + simpleTest(sqlite) + + using(sqlite.createStatement()) { statement => + assert(getVersion(statement, "network", 2) == 2) + } + } test("add/remove/list nodes") { @@ -80,7 +106,6 @@ class SqliteNetworkDbSpec extends FunSuite { } def simpleTest(sqlite: Connection) = { - val sqlite = inmem val db = new SqliteNetworkDb(sqlite) def sig = Crypto.encodeSignature(Crypto.sign(randomKey.toBin, randomKey)) :+ 1.toByte From b0c67e796b52ce67c1d948e7a66b983dc1af873d Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 18 Mar 2019 20:15:45 +0100 Subject: [PATCH 52/86] added default value to Graph --- .../scala/fr/acinq/eclair/router/Graph.scala | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 56018fe141..fbeb8b0a17 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -436,10 +436,8 @@ object Graph { * @return all the edges going from keyA --> keyB (there might be more than one if it refers to different shortChannelId) */ def getEdgesBetween(keyA: PublicKey, keyB: PublicKey): Seq[GraphEdge] = { - vertices.get(keyB) match { - case None => Seq.empty - case Some(adj) => adj.filter(e => e.desc.a == keyA) - } + // NB: graph has an empty list as default value + vertices(keyB).filter(e => e.desc.a == keyA) } /** @@ -449,7 +447,8 @@ object Graph { * @return */ def getIncomingEdgesOf(keyB: PublicKey): Seq[GraphEdge] = { - vertices.getOrElse(keyB, List.empty) + // NB: graph has an empty list as default value + vertices(keyB) } /** @@ -536,35 +535,25 @@ object Graph { def makeGraph(channels: SortedMap[ShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization - val mutableMap = new {} with mutable.HashMap[PublicKey, List[GraphEdge]] { - override def initialSize: Int = channels.size + 1 + val mutableMap = { + val m = new mutable.HashMap[PublicKey, List[GraphEdge]] { + override def initialSize: Int = channels.size + 1 + } + m.withDefaultValue(List.empty[GraphEdge]) // with this we can always call m(x) } // add all the vertices and edges in one go channels.values.foreach { channel => - // make desc for both directions - val (desc1, desc2) = ( - channel.update_1_opt.map(u1 => Router.getDesc(u1, channel.ann)), - channel.update_2_opt.map(u2 => Router.getDesc(u2, channel.ann)) - ) - - desc1.map { descAB => - mutableMap.put(descAB.b, GraphEdge(descAB, channel.update_1_opt.get) +: mutableMap.getOrElse(descAB.b, List.empty[GraphEdge])) - mutableMap.get(descAB.a) match { - case None => mutableMap += descAB.a -> List.empty[GraphEdge] - case _ => - } + def addToGraph(u: ChannelUpdate) = { + val desc = Router.getDesc(u, channel.ann) + mutableMap.put(desc.b, GraphEdge(desc, u) +: mutableMap(desc.b)) } - desc2.map { descBA => - mutableMap.put(descBA.b, GraphEdge(descBA, channel.update_2_opt.get) +: mutableMap.getOrElse(descBA.b, List.empty[GraphEdge])) - mutableMap.get(descBA.a) match { - case None => mutableMap += descBA.a -> List.empty[GraphEdge] - case _ => - } + // make desc for both directions + channel.update_1_opt.foreach(addToGraph) + channel.update_2_opt.foreach(addToGraph) - } } new DirectedGraph(mutableMap.toMap) From 553acce5fc82f8a092f8fec3e594f0611839e836 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 18 Mar 2019 20:39:31 +0100 Subject: [PATCH 53/86] Revert "added default value to Graph" This reverts commit b0c67e796b52ce67c1d948e7a66b983dc1af873d. --- .../scala/fr/acinq/eclair/router/Graph.scala | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index fbeb8b0a17..56018fe141 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -436,8 +436,10 @@ object Graph { * @return all the edges going from keyA --> keyB (there might be more than one if it refers to different shortChannelId) */ def getEdgesBetween(keyA: PublicKey, keyB: PublicKey): Seq[GraphEdge] = { - // NB: graph has an empty list as default value - vertices(keyB).filter(e => e.desc.a == keyA) + vertices.get(keyB) match { + case None => Seq.empty + case Some(adj) => adj.filter(e => e.desc.a == keyA) + } } /** @@ -447,8 +449,7 @@ object Graph { * @return */ def getIncomingEdgesOf(keyB: PublicKey): Seq[GraphEdge] = { - // NB: graph has an empty list as default value - vertices(keyB) + vertices.getOrElse(keyB, List.empty) } /** @@ -535,25 +536,35 @@ object Graph { def makeGraph(channels: SortedMap[ShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization - val mutableMap = { - val m = new mutable.HashMap[PublicKey, List[GraphEdge]] { - override def initialSize: Int = channels.size + 1 - } - m.withDefaultValue(List.empty[GraphEdge]) // with this we can always call m(x) + val mutableMap = new {} with mutable.HashMap[PublicKey, List[GraphEdge]] { + override def initialSize: Int = channels.size + 1 } // add all the vertices and edges in one go channels.values.foreach { channel => - def addToGraph(u: ChannelUpdate) = { - val desc = Router.getDesc(u, channel.ann) - mutableMap.put(desc.b, GraphEdge(desc, u) +: mutableMap(desc.b)) + // make desc for both directions + val (desc1, desc2) = ( + channel.update_1_opt.map(u1 => Router.getDesc(u1, channel.ann)), + channel.update_2_opt.map(u2 => Router.getDesc(u2, channel.ann)) + ) + + desc1.map { descAB => + mutableMap.put(descAB.b, GraphEdge(descAB, channel.update_1_opt.get) +: mutableMap.getOrElse(descAB.b, List.empty[GraphEdge])) + mutableMap.get(descAB.a) match { + case None => mutableMap += descAB.a -> List.empty[GraphEdge] + case _ => + } } - // make desc for both directions - channel.update_1_opt.foreach(addToGraph) - channel.update_2_opt.foreach(addToGraph) + desc2.map { descBA => + mutableMap.put(descBA.b, GraphEdge(descBA, channel.update_2_opt.get) +: mutableMap.getOrElse(descBA.b, List.empty[GraphEdge])) + mutableMap.get(descBA.a) match { + case None => mutableMap += descBA.a -> List.empty[GraphEdge] + case _ => + } + } } new DirectedGraph(mutableMap.toMap) From f5ced64239bac3e2cf5d7671bfcac92a854948f3 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 19 Mar 2019 12:08:15 +0100 Subject: [PATCH 54/86] Extract graph edges creation operations in a function --- .../scala/fr/acinq/eclair/router/Graph.scala | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 56018fe141..6f8022ce78 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -542,28 +542,22 @@ object Graph { // add all the vertices and edges in one go channels.values.foreach { channel => - - // make desc for both directions - val (desc1, desc2) = ( - channel.update_1_opt.map(u1 => Router.getDesc(u1, channel.ann)), - channel.update_2_opt.map(u2 => Router.getDesc(u2, channel.ann)) - ) - - desc1.map { descAB => - mutableMap.put(descAB.b, GraphEdge(descAB, channel.update_1_opt.get) +: mutableMap.getOrElse(descAB.b, List.empty[GraphEdge])) - mutableMap.get(descAB.a) match { - case None => mutableMap += descAB.a -> List.empty[GraphEdge] - case _ => - } + channel.update_1_opt.foreach { u1 => + val desc1 = Router.getDesc(u1, channel.ann) + addDescToMap(desc1, u1) } - desc2.map { descBA => - mutableMap.put(descBA.b, GraphEdge(descBA, channel.update_2_opt.get) +: mutableMap.getOrElse(descBA.b, List.empty[GraphEdge])) - mutableMap.get(descBA.a) match { - case None => mutableMap += descBA.a -> List.empty[GraphEdge] - case _ => - } + channel.update_2_opt.foreach { u2 => + val desc2 = Router.getDesc(u2, channel.ann) + addDescToMap(desc2, u2) + } + } + def addDescToMap(desc: ChannelDesc, u: ChannelUpdate) = { + mutableMap.put(desc.b, GraphEdge(desc, u) +: mutableMap.getOrElse(desc.b, List.empty[GraphEdge])) + mutableMap.get(desc.a) match { + case None => mutableMap += desc.a -> List.empty[GraphEdge] + case _ => } } From 94711a87fe30ecc185f8d4df90165be6f7e0bcd3 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 19 Mar 2019 14:59:21 +0100 Subject: [PATCH 55/86] added test vectors --- .../scala/fr/acinq/eclair/router/Router.scala | 2 +- .../eclair/wire/LightningMessageTypes.scala | 2 +- .../router/RoutingSyncWithChecksumsSpec.scala | 2 +- .../wire/LightningMessageCodecsSpec.scala | 58 ++++++++++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) 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 af865a097b..40d145752f 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 @@ -437,7 +437,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // ask for everything // we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't // have to worry about sending a new query_channel_range when another query is still in progress - val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue, optionExtendedQueryFlags_opt = flags_opt) + val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue, extendedQueryFlags_opt = flags_opt) log.info("sending query_channel_range={}", query) remote ! query 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 951f4ad07d..5d49111f44 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 @@ -270,7 +270,7 @@ object ExtendedQueryFlags { case class QueryChannelRange(chainHash: ByteVector32, firstBlockNum: Long, numberOfBlocks: Long, - optionExtendedQueryFlags_opt: Option[ExtendedQueryFlags]) extends RoutingMessage with HasChainHash + extendedQueryFlags_opt: Option[ExtendedQueryFlags]) extends RoutingMessage with HasChainHash case class ReplyChannelRange(chainHash: ByteVector32, firstBlockNum: Long, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala index 7c218de78f..f2733cdbbe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala @@ -145,7 +145,7 @@ class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with Fun } // ask router to send a channel range query sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS))) - assert(sender.expectMsgType[QueryChannelRange].optionExtendedQueryFlags_opt.isDefined) + assert(sender.expectMsgType[QueryChannelRange].extendedQueryFlags_opt.isDefined) sender.expectMsgType[GossipTimestampFilter] // send back all our ids and timestamps 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 3c59122a05..7889b196e6 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 @@ -22,9 +22,14 @@ import com.google.common.net.InetAddresses import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} import fr.acinq.eclair._ +import fr.acinq.eclair.api._ +import fr.acinq.eclair.channel.State import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ +import org.json4s.JsonAST.{JNothing, JString} +import org.json4s.{CustomSerializer, ShortTypeHints} +import org.json4s.jackson.Serialization import org.scalatest.FunSuite import scodec.bits.{BitVector, ByteVector, HexStringSyntax} @@ -262,7 +267,7 @@ class LightningMessageCodecsSpec extends FunSuite { test("non-reg encoding type") { val refs = Map( - hex"0105 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4000400010204" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3000c01789c6364620100000e0008" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) @@ -273,6 +278,57 @@ class LightningMessageCodecsSpec extends FunSuite { } } + case class TestItem(msg: Any, hex: String) + + test("test vectors") { + + val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, None) + val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 35000, 100, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) + val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 756230, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), None) + val reply_channel_range_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 1600, 110, 1, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), None) + val reply_channel_range_timestamps_checksums = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 122334, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(164545, 1111, 948165, 2222), TimestampsAndChecksums(489645, 3333, 4786864, 4444), TimestampsAndChecksums(46456, 5555, 9788415, 6666))))) + val reply_channel_range_timestamps_checksums_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 500, 100, 1, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(1234545), ShortChannelId(4897484), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(164545, 1111, 948165, 2222), TimestampsAndChecksums(489645, 3333, 4786864, 4444), TimestampsAndChecksums(46456, 5555, 9788415, 6666))))) + val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None) + val query_short_channel_id_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), None) + val query_short_channel_id_flags = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), Some(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))), Some(EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) + + val refs = List( + query_channel_range, + query_channel_range_timestamps_checksums, + reply_channel_range, + reply_channel_range_zlib, + reply_channel_range_timestamps_checksums, + reply_channel_range_timestamps_checksums_zlib, + query_short_channel_id, + query_short_channel_id_zlib, + query_short_channel_id_flags, + query_short_channel_id_flags_zlib + ) + + class EncodingTypeSerializer extends CustomSerializer[EncodingType](format => ({ null }, { + case EncodingType.UNCOMPRESSED => JString("UNCOMPRESSED") + case EncodingType.COMPRESSED_ZLIB => JString("COMPRESSED_ZLIB") + })) + + class ExtendedQueryFlagsSerializer extends CustomSerializer[ExtendedQueryFlags](format => ({ null }, { + case ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS => JString("TIMESTAMPS_AND_CHECKSUMS") + })) + + implicit val formats = org.json4s.DefaultFormats.withTypeHintFieldName("type") + new EncodingTypeSerializer + new ExtendedQueryFlagsSerializer + new ByteVectorSerializer + new ByteVector32Serializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer +new PaymentRequestSerializer + + ShortTypeHints(List( + classOf[QueryChannelRange], + classOf[ReplyChannelRange], + classOf[QueryShortChannelIds])) + + refs.foreach { + obj => + val bin = lightningMessageCodec.encode(obj).require + println(Serialization.writePretty(TestItem(obj, bin.toHex))) + } + + } + test("encode/decode per-hop payload") { val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) val bin = LightningMessageCodecs.perHopPayloadCodec.encode(payload).require From a18ec5588b1418e5d06c9bf732b6d56fb9480f41 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 19 Mar 2019 15:48:08 +0100 Subject: [PATCH 56/86] ignore fake test vector test --- .../scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7889b196e6..4118893f19 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 @@ -280,7 +280,7 @@ class LightningMessageCodecsSpec extends FunSuite { case class TestItem(msg: Any, hex: String) - test("test vectors") { + ignore("test vectors") { val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, None) val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 35000, 100, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) From f2a18be676fde0eb34bbc01e0b848b7cc0019045 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 19 Mar 2019 17:23:00 +0100 Subject: [PATCH 57/86] added tests on query flags --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 1 - .../scala/fr/acinq/eclair/router/Router.scala | 5 +- .../eclair/wire/LightningMessageTypes.scala | 1 + .../router/ChannelRangeQueriesSpec.scala | 61 +++++++++++++++++++ .../eclair/router/RouteCalculationSpec.scala | 4 +- 5 files changed, 68 insertions(+), 4 deletions(-) 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 c1c659839f..6d4612dc63 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 @@ -121,7 +121,6 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor val remoteFeatures = Features(ByteVector(remoteInit.localFeatures.toArray).toBitVector) log.info(s"peer is using globalFeatures=${remoteInit.globalFeatures.toBin} and localFeatures=${remoteInit.localFeatures.toBin}") - log.info(s"peer has globalFeatures=${remoteInit.globalFeatures} localFeatures=${remoteInit.localFeatures}: initialRoutingSync=${remoteFeatures.hasInitialRoutingSync} channelRangeQueriesOptional=${remoteFeatures.hasChannelRangeQueriesOptional} channelRangeQueriesMandatory=${remoteFeatures.hasChannelRangeQueriesMandatory}") if (remoteFeatures.areSupported) { d.origin_opt.foreach(origin => origin ! "connected") 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 40d145752f..3dfcf1c1d0 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 @@ -560,7 +560,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom case ((c, u), (shortChannelId, idx)) => var c1 = c var u1 = u - val flag = queryFlags_opt.map(_.array(idx)).getOrElse((QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte) + val flag = queryFlags_opt.map(_.array(idx)).getOrElse(QueryFlagTypes.INCLUDE_ALL) d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => @@ -845,6 +845,9 @@ object Router { // - it is not stale itself if (ourInfo.timestamp1 < theirInfo.timestamp1 && (ourInfo.checksum1 != theirInfo.checksum1 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp1)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + case None if channels.contains(shortChannelId) => + // we know this channel: we only request their channel updates + flag = QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 case _ => // we don't know this channel: we request everything flag = QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 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 5d49111f44..c1aedc0894 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 @@ -239,6 +239,7 @@ case object QueryFlagTypes { val INCLUDE_CHANNEL_ANNOUNCEMENT: Byte = 1 val INCLUDE_CHANNEL_UPDATE_1: Byte = 2 val INCLUDE_CHANNEL_UPDATE_2: Byte = 4 + val INCLUDE_ALL: Byte = (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte def includeAnnouncement(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT) != 0 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 25c642de69..f675657835 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -20,6 +20,11 @@ import fr.acinq.eclair.ShortChannelId import org.scalatest.FunSuite import scala.collection.{SortedSet, immutable} +import fr.acinq.eclair.randomKey +import fr.acinq.eclair.wire.{ExtendedInfo, QueryFlagTypes, TimestampsAndChecksums} + +import scala.collection.immutable.SortedMap +import scala.compat.Platform import scala.util.Random @@ -30,6 +35,62 @@ class ChannelRangeQueriesSpec extends FunSuite { val timestamp = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap val timestamps = shortChannelIds.map(id => id -> (random.nextInt(400000).toLong, random.nextInt(400000).toLong)).toMap + test("compute flag tests") { + + val now = Platform.currentTime / 1000 + + val a = randomKey.publicKey + val b = randomKey.publicKey + val ab = RouteCalculationSpec.makeChannel(123466L, a, b) + val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, 0, 0, timestamp = now) + val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, 0, 0, timestamp = now) + + val c = randomKey.publicKey + val d = randomKey.publicKey + val cd = RouteCalculationSpec.makeChannel(451312L, c, d) + val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, 0, 0, timestamp = now) + val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, 0, 0, timestamp = now) + + val e = randomKey.publicKey + val f = randomKey.publicKey + val ef = RouteCalculationSpec.makeChannel(167514L, e, f) + + val channels = SortedMap( + ab.shortChannelId -> ab, + cd.shortChannelId -> cd + ) + + val updates = Map( + ab1 -> uab1, + ab2 -> uab2, + cd1 -> ucd1 + ) + + import fr.acinq.eclair.wire.QueryFlagTypes._ + + assert(Router.getChannelDigestInfo(channels, updates)(ab.shortChannelId) == TimestampsAndChecksums(now, 714408668, now, 714408668)) + + // no extended info but we know the channel: we ask for the updates + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + // same checksums, newer timestamps: we don't ask anything + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now + 1, 714408668, now + 1, 714408668))) === 0.toByte) + // different checksums, newer timestamps: we ask for the updates + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now + 1, 154654604, now, 714408668))) === INCLUDE_CHANNEL_UPDATE_1) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now, 714408668, now + 1, 45664546))) === INCLUDE_CHANNEL_UPDATE_2) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now + 1, 154654604, now + 1, 45664546+6))) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + // different checksums, older timestamps: we don't ask anything + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now - 1, 154654604, now, 714408668))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now, 714408668, now - 1, 45664546))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now - 1, 154654604, now - 1, 45664546))) === 0.toByte) + + // missing channel update: we ask for it + assert(Router.computeFlag(channels, updates)(cd.shortChannelId, Some(TimestampsAndChecksums(now, 714408668, now, 714408668))) === INCLUDE_CHANNEL_UPDATE_2) + + // unknown channel: we ask everything + assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None) === QueryFlagTypes.INCLUDE_ALL) + + } + /*test("create `reply_channel_range` messages (uncompressed format)") { val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 3b270ebc68..18c95db8e7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -858,12 +858,12 @@ object RouteCalculationSpec { makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBaseMsat, feeProportionalMillionth, minHtlcMsat, maxHtlcMsat, cltvDelta) } - def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBaseMsat: Int, feeProportionalMillionth: Int, minHtlcMsat: Long = DEFAULT_AMOUNT_MSAT, maxHtlcMsat: Option[Long] = None, cltvDelta: Int = 0): (ChannelDesc, ChannelUpdate) = + def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBaseMsat: Int, feeProportionalMillionth: Int, minHtlcMsat: Long = DEFAULT_AMOUNT_MSAT, maxHtlcMsat: Option[Long] = None, cltvDelta: Int = 0, timestamp: Long = 0): (ChannelDesc, ChannelUpdate) = ChannelDesc(shortChannelId, nodeId1, nodeId2) -> ChannelUpdate( signature = DUMMY_SIG, chainHash = Block.RegtestGenesisBlock.hash, shortChannelId = shortChannelId, - timestamp = 0L, + timestamp = timestamp, messageFlags = maxHtlcMsat match { case Some(_) => 1 case None => 0 From e6a280551602d6d725dcdd6e806ab9607472b904 Mon Sep 17 00:00:00 2001 From: pm47 Date: Sun, 24 Mar 2019 19:13:44 +0100 Subject: [PATCH 58/86] cleanup in codecs --- .../fr/acinq/eclair/wire/LightningMessageCodecs.scala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 ee3f563fd5..688f41f624 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 @@ -81,7 +81,7 @@ object LightningMessageCodecs { ) def scalar: Codec[Scalar] = Codec[Scalar]( - (value: Scalar) => bytes(32).encode(ByteVector(value.toBin.toArray)), + (value: Scalar) => bytes(32).encode(value.toBin), (wire: BitVector) => bytes(32).decode(wire).map(_.map(b => Scalar(b))) ) @@ -90,11 +90,6 @@ object LightningMessageCodecs { (wire: BitVector) => bytes(33).decode(wire).map(_.map(b => Point(b))) ) - def privateKey: Codec[PrivateKey] = Codec[PrivateKey]( - (priv: PrivateKey) => bytes(32).encode(priv.value.toBin), - (wire: BitVector) => bytes(32).decode(wire).map(_.map(b => PrivateKey(b, compressed = true))) - ) - def publicKey: Codec[PublicKey] = Codec[PublicKey]( (pub: PublicKey) => bytes(33).encode(pub.value.toBin(compressed = true)), (wire: BitVector) => bytes(33).decode(wire).map(_.map(b => PublicKey(b))) From 0ace8da6218b6b87f0d7c7aa3b3b416b60194e69 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 26 Mar 2019 18:22:04 +0100 Subject: [PATCH 59/86] fixed and improved tests --- .../scala/fr/acinq/eclair/router/Router.scala | 38 +-- .../eclair/wire/LightningMessageTypes.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 6 +- .../router/ChannelRangeQueriesSpec.scala | 101 +------ .../acinq/eclair/router/RoutingSyncSpec.scala | 273 +++++++++++++----- .../router/RoutingSyncWithChecksumsSpec.scala | 163 ----------- 6 files changed, 228 insertions(+), 355 deletions(-) delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala 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 8c2fddbeb5..685aa31af9 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 @@ -111,7 +111,7 @@ case object TickPruneStaleChannels * Created by PM on 24/05/2016. */ -class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Promise[Done]] = None) extends FSMDiagnosticActorLogging[State, Data] { +class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Promise[Done]] = None) extends FSMDiagnosticActorLogging[State, Data] { import Router._ @@ -507,9 +507,9 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom log.debug("received node announcement for nodeId={}", n.nodeId) stay using handle(n, sender, d) - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, optionExtendedQueryFlags_opt)), d) => + case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, extendedQueryFlags_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) - log.info("received {}", routingMessage) + log.info("received query_channel_range with firstBlockNum={} numberOfBlocks={} extendedQueryFlags_opt={}", firstBlockNum, numberOfBlocks, extendedQueryFlags_opt) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) @@ -518,13 +518,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom transport ! ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, shortChannelIds = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, chunk.shortChannelIds), - optionExtendedQueryFlags_opt = optionExtendedQueryFlags_opt, - extendedInfo_opt = optionExtendedQueryFlags_opt map { + extendedQueryFlags_opt = extendedQueryFlags_opt, + extendedInfo_opt = extendedQueryFlags_opt map { case ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS => ExtendedInfo(chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates))) })) stay - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, shortChannelIds, optionExtendedQueryFlags_opt, extendedInfo_opt)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, shortChannelIds, extendedQueryFlags_opt, extendedInfo_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) val shortChannelIdAndFlags = shortChannelIds.array .zipWithIndex @@ -536,13 +536,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom val u1 = u + (if (QueryFlagTypes.includeUpdate1(flag)) 1 else 0) + (if (QueryFlagTypes.includeUpdate2(flag)) 1 else 0) (c1, u1) } - log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={} queryFlags=${optionExtendedQueryFlags_opt.getOrElse("n/a")}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) + log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={} queryFlags=${extendedQueryFlags_opt.getOrElse("n/a")}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) val replies = shortChannelIdAndFlags .grouped(SHORTID_WINDOW) .map(chunk => QueryShortChannelIds(chainHash, shortChannelIds = EncodedShortChannelIds(shortChannelIds.encoding, chunk.map(_.shortChannelId)), - queryFlags_opt = optionExtendedQueryFlags_opt map { + queryFlags_opt = extendedQueryFlags_opt map { case _ => EncodedQueryFlags(shortChannelIds.encoding, chunk.map(_.flag)) })) .toList @@ -940,15 +940,19 @@ object Router { */ def split(shortChannelIds: SortedSet[ShortChannelId]): List[ShortChannelIdsChunk] = { // TODO: this is wrong because it can split blocks - shortChannelIds - .grouped(2000) // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - .toList - .map { group => - // NB: group is never empty - val firstBlock: Long = ShortChannelId.coordinates(group.head).blockHeight.toLong - val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong - firstBlock + 1 - ShortChannelIdsChunk(firstBlock, numBlocks, group.toList) - } + if (shortChannelIds.isEmpty) { + List(ShortChannelIdsChunk(0, 0, List.empty)) + } else { + shortChannelIds + .grouped(2000) // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + .toList + .map { group => + // NB: group is never empty + val firstBlock: Long = ShortChannelId.coordinates(group.head).blockHeight.toLong + val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong - firstBlock + 1 + ShortChannelIdsChunk(firstBlock, numBlocks, group.toList) + } + } } def updateSync(syncMap: Map[PublicKey, Sync], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Sync], Option[RoutingMessage]) = { 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 c1aedc0894..b1c631d5f2 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 @@ -278,7 +278,7 @@ case class ReplyChannelRange(chainHash: ByteVector32, numberOfBlocks: Long, complete: Byte, shortChannelIds: EncodedShortChannelIds, - optionExtendedQueryFlags_opt: Option[ExtendedQueryFlags], + extendedQueryFlags_opt: Option[ExtendedQueryFlags], extendedInfo_opt: Option[ExtendedInfo]) extends RoutingMessage with HasChainHash { extendedInfo_opt.foreach(extendedInfo => require(shortChannelIds.array.size == extendedInfo.array.size, s"shortChannelIds.size=${shortChannelIds.array.size} != extendedInfo.size=${extendedInfo.array.size}")) } 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 f0529eb6c7..f3d16487d7 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 @@ -26,7 +26,7 @@ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements, SendPing} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router.{ChannelRangeQueriesSpec, Rebroadcast} +import fr.acinq.eclair.router.{ChannelRangeQueriesSpec, Rebroadcast, RoutingSyncSpec} import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingType, Error, Ping, Pong} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, randomBytes, wire} import org.scalatest.Outcome @@ -35,11 +35,11 @@ import scala.concurrent.duration._ class PeerSpec extends TestkitBaseClass { - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(100) + val shortChannelIds = RoutingSyncSpec.shortChannelIds.take(100) val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) val channels = fakeRoutingInfo.map(_._1).toList val updates = (fakeRoutingInfo.map(_._2) ++ fakeRoutingInfo.map(_._3)).toList - val nodes = (fakeRoutingInfo.map(_._4) ++ fakeRoutingInfo.map(_._5)).toList + val nodes = fakeRoutingInfo.toList.map(_._4) ++ fakeRoutingInfo.toList.map(_._5) case class FixtureParam(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index f675657835..e8e0820e21 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -16,26 +16,17 @@ package fr.acinq.eclair.router -import fr.acinq.eclair.ShortChannelId -import org.scalatest.FunSuite - -import scala.collection.{SortedSet, immutable} import fr.acinq.eclair.randomKey -import fr.acinq.eclair.wire.{ExtendedInfo, QueryFlagTypes, TimestampsAndChecksums} +import fr.acinq.eclair.wire._ +import org.scalatest.FunSuite import scala.collection.immutable.SortedMap import scala.compat.Platform -import scala.util.Random class ChannelRangeQueriesSpec extends FunSuite { - import ChannelRangeQueriesSpec._ - val random = new Random() - val timestamp = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap - val timestamps = shortChannelIds.map(id => id -> (random.nextInt(400000).toLong, random.nextInt(400000).toLong)).toMap - - test("compute flag tests") { + test("compute flag tests") { val now = Platform.currentTime / 1000 @@ -90,90 +81,4 @@ class ChannelRangeQueriesSpec extends FunSuite { assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None) === QueryFlagTypes.INCLUDE_ALL) } - - /*test("create `reply_channel_range` messages (uncompressed format)") { - val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ ShortChannelIdsBlock.decode(reply.data)._2) - assert(decoded == shortChannelIds) - } - - test("create `reply_channel_range` messages (ZLIB format)") { - val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ { - val (ChannelRangeQueries.ZLIB_FORMAT, ids, false) = ShortChannelIdsBlock.decode(reply.data) - ids - }) - assert(decoded == shortChannelIds) - } - - test("create `reply_channel_range` messages (GZIP format)") { - val blocks = ShortChannelIdsBlock.encode(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = true) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ { - val (ChannelRangeQueries.ZLIB_FORMAT, ids, true) = ShortChannelIdsBlock.decode(reply.data) - ids - }) - assert(decoded == shortChannelIds) - } - - test("create empty `reply_channel_range` message") { - val blocks = ShortChannelIdsBlock.encode(400000, 20000, SortedSet.empty[ShortChannelId], ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ { - val (format, ids, false) = ShortChannelIdsBlock.decode(reply.data) - ids - }) - assert(decoded.isEmpty) - } - - test("create `reply_channel_range_ex` messages (uncompressed format)") { - val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamp(id) == decoded(id)) - } - - test("create `reply_channel_range_ex` messages (zlib format)") { - val blocks = ShortChannelIdAndTimestampBlock.encode(400000, 20000, shortChannelIds, id => timestamp(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampBlock.decode(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamp(id) == decoded(id)) - } - - test("create `reply_channel_range_ex2` messages (uncompressed format)") { - val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] - replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamp(id) == decoded(id)) - } - - test("create `reply_channel_range_ex2` messages (zlib format)") { - val blocks = ShortChannelIdAndTimestampsBlock.encode(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeDeprecated(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, (Long, Long)] - replies.foreach(reply => decoded = decoded ++ ShortChannelIdAndTimestampsBlock.decode(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamp(id) == decoded(id)) - }*/ - -} - -object ChannelRangeQueriesSpec { - lazy val shortChannelIds: immutable.SortedSet[ShortChannelId] = (for { - block <- 400000 to 420000 - txindex <- 0 to 5 - outputIndex <- 0 to 1 - } yield ShortChannelId(block, txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index bbc20d44cc..e82aaf2540 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -16,83 +16,196 @@ package fr.acinq.eclair.router -import akka.actor.ActorSystem +import akka.actor.{Actor, ActorSystem, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{Block, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ import org.scalatest.FunSuiteLike +import scala.collection.immutable.TreeMap +import scala.collection.{SortedSet, immutable, mutable} import scala.compat.Platform -import scala.concurrent.duration._ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { - import RoutingSyncSpec.makeFakeRoutingInfo - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - - test("handle channel range queries") { - val params = TestConstants.Alice.nodeParams - val router = TestFSMRef(new Router(params, TestProbe().ref)) - val transport = TestProbe() - val sender = TestProbe() - sender.ignoreMsg { case _: TransportHandler.ReadAck => true } - val remoteNodeId = TestConstants.Bob.nodeParams.nodeId - - // ask router to send a channel range query - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, flags_opt = None)) - val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] - sender.expectMsgType[GossipTimestampFilter] - - // split our answer in 3 blocks - val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) - val block2 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.drop(100).take(100).toList), None, None) - - // send first block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) - // router should ask for our first block of ids - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.shortChannelIds, None)) - - // send second block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block2)) - - // router should not ask for more ids, it already has a pending query ! - sender.expectNoMsg(1 second) - - // send the first 50 items - block1.shortChannelIds.array.take(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) + import RoutingSyncSpec._ + + val fakeRoutingInfo: TreeMap[ShortChannelId, (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec + .shortChannelIds + .take(2345) + .foldLeft(TreeMap.empty[ShortChannelId, (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement)]) { + case (m, shortChannelId) => m + (shortChannelId -> makeFakeRoutingInfo(shortChannelId)) + } + + class YesWatcher extends Actor { + override def receive: Receive = { + case ValidateRequest(c) => + val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) + val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) + val fakeFundingTx = Transaction( + version = 2, + txIn = Seq.empty[TxIn], + txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format + lockTime = 0) + sender ! ValidateResult(c, Right(fakeFundingTx, UtxoStatus.Unspent)) + } + } - sender.expectNoMsg(1 second) + case class BasicSyncResult(ranges: Int, queries: Int, channels: Int, updates: Int) - // send the last 50 items - block1.shortChannelIds.array.drop(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) + case class SyncResult(ranges: Seq[ReplyChannelRange], queries: Seq[QueryShortChannelIds], channels: Seq[ChannelAnnouncement], updates: Seq[ChannelUpdate]) { + def counts = BasicSyncResult(ranges.size, queries.size, channels.size, updates.size) + } - // during that time, router should not have asked for more ids, it already has a pending query ! - transport.expectNoMsg(200 millis) + def sync(src: TestFSMRef[State, Data, Router], tgt: TestFSMRef[State, Data, Router], extendedQueryFlags_opt: Option[ExtendedQueryFlags]): SyncResult = { + val sender = TestProbe() + val pipe = TestProbe() + pipe.ignoreMsg { + case _: TransportHandler.ReadAck => true + case _: GossipTimestampFilter => true + } + val srcId = src.underlyingActor.nodeParams.nodeId + val tgtId = tgt.underlyingActor.nodeParams.nodeId + sender.send(src, SendChannelQuery(tgtId, pipe.ref, extendedQueryFlags_opt)) + // src sends a query_channel_range to bob + val qcr = pipe.expectMsgType[QueryChannelRange] + pipe.send(tgt, PeerRoutingMessage(pipe.ref, srcId, qcr)) + // this allows us to know when the last reply_channel_range has been set + pipe.send(tgt, 'data) + // tgt answers with reply_channel_ranges + val rcrs = pipe.receiveWhile() { + case rcr: ReplyChannelRange => rcr + } + pipe.expectMsgType[Data] + rcrs.foreach(rcr => pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, rcr))) + // then src will now query announcements + var queries = Vector.empty[QueryShortChannelIds] + var channels = Vector.empty[ChannelAnnouncement] + var updates = Vector.empty[ChannelUpdate] + while (src.stateData.sync.nonEmpty) { + // for each chunk, src sends a query_short_channel_id + val query = pipe.expectMsgType[QueryShortChannelIds] + pipe.send(tgt, PeerRoutingMessage(pipe.ref, srcId, query)) + queries = queries :+ query + val announcements = pipe.receiveWhile() { + case c: ChannelAnnouncement => + channels = channels :+ c + c + case u: ChannelUpdate => + updates = updates :+ u + u + } + // tgt replies with announcements + announcements.foreach(ann => pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, ann))) + // and tgt ends this chunk with a reply_short_channel_id_end + val rscie = pipe.expectMsgType[ReplyShortChannelIdsEnd] + pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, rscie)) + } + SyncResult(rcrs, queries, channels, updates) + } - // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) + test("handle channel range extended") { + val watcher = system.actorOf(Props(new YesWatcher())) + val alice = TestFSMRef(new Router(Alice.nodeParams, watcher)) + val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) + val charlieId = randomKey.publicKey + val sender = TestProbe() + val extendedQueryFlags_opt = None + + // tell alice to sync with bob + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + + // add some channels and updates to bob and resync + fakeRoutingInfo.take(40).map(_._2._1).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + fakeRoutingInfo.take(40).map(_._2._2).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 40) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + + // add some updates to bob and resync + fakeRoutingInfo.take(40).map(_._2._3).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 80) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 80) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + + // add everything (duplicates will be ignored) + fakeRoutingInfo.values.foreach { + case (c, u1, u2, _, _) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u1)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u2)) + } + awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size) + assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + } - // router should ask for our second block of ids - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block2.shortChannelIds, None)) + test("handle channel range extended (extended)") { + val watcher = system.actorOf(Props(new YesWatcher())) + val alice = TestFSMRef(new Router(Alice.nodeParams, watcher)) + val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) + val charlieId = randomKey.publicKey + val sender = TestProbe() + val extendedQueryFlags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS) + + // tell alice to sync with bob + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + + // add some channels and updates to bob and resync + fakeRoutingInfo.take(40).map(_._2._1).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + fakeRoutingInfo.take(40).map(_._2._2).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 40) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + + // add some updates to bob and resync + fakeRoutingInfo.take(40).map(_._2._3).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 80) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 0, updates = 40) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + + // add everything (duplicates will be ignored) + fakeRoutingInfo.values.foreach { + case (c, u1, u2, _, _) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u1)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u2)) + } + awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size) + assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40)) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) + + // bump random channel_updates + def touchUpdate(shortChannelId: Int, side: Boolean) = { + val (c, u1, u2, _, _) = fakeRoutingInfo.values.toList(shortChannelId) + makeNewerChannelUpdate(c, if (side) u1 else u2) + } + + val bumpedUpdates = (List(0, 42, 147, 153, 654, 834, 2301).map(touchUpdate(_, true)) ++ List(1, 42, 150, 200).map(touchUpdate(_, false))).toSet + bumpedUpdates.foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + assert(BasicSyncResult(ranges = 2, queries = 2, channels = 0, updates = bumpedUpdates.size) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(alice.stateData.updates === bob.stateData.updates) } test("reset sync state on reconnection") { @@ -108,7 +221,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.take(100).toList), None, None) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, fakeRoutingInfo.take(100).keys.toList), None, None) // send first block sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) @@ -149,26 +262,40 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { object RoutingSyncSpec { - val (priv_a, priv_b, priv_funding_a, priv_funding_b) = (randomKey, randomKey, randomKey, randomKey) - val (priv1, priv2) = if (Announcements.isNode1(priv_a.publicKey, priv_b.publicKey)) - (priv_a, priv_b) - else - (priv_b, priv_a) + + lazy val shortChannelIds: immutable.SortedSet[ShortChannelId] = (for { + block <- 400000 to 420000 + txindex <- 0 to 5 + outputIndex <- 0 to 1 + } yield ShortChannelId(block, txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) + + // this map will store private keys so that we can sign new announcements at will + val pub2priv: mutable.Map[PublicKey, PrivateKey] = mutable.HashMap.empty + + val unused = randomKey def makeFakeRoutingInfo(shortChannelId: ShortChannelId): (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement) = { val timestamp = Platform.currentTime / 1000 - val channelAnn_ab = channelAnnouncement(shortChannelId, priv_a, priv_b, priv_funding_a, priv_funding_b) - val TxCoordinates(blockHeight, _, _) = ShortChannelId.coordinates(shortChannelId) - val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_b.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) - val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, priv_a.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) - val nodeAnnouncement_a = makeNodeAnnouncement(priv_a, "a", Color(0, 0, 0), List()) - val nodeAnnouncement_b = makeNodeAnnouncement(priv_b, "b", Color(0, 0, 0), List()) - (channelAnn_ab, channelUpdate_ab, channelUpdate_ba, nodeAnnouncement_a, nodeAnnouncement_b) + val (priv1, priv2) = { + val (priv_a, priv_b) = (randomKey, randomKey) + if (Announcements.isNode1(priv_a.publicKey, priv_b.publicKey)) (priv_a, priv_b) else (priv_b, priv_a) + } + val priv_funding1 = unused + val priv_funding2 = unused + pub2priv += (priv1.publicKey -> priv1) + pub2priv += (priv2.publicKey -> priv2) + val channelAnn_12 = channelAnnouncement(shortChannelId, priv1, priv2, priv_funding1, priv_funding2) + val channelUpdate_12 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv1, priv2.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) + val channelUpdate_21 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv2, priv1.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) + val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "", Color(0, 0, 0), List()) + val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "", Color(0, 0, 0), List()) + (channelAnn_12, channelUpdate_12, channelUpdate_21, nodeAnnouncement_1, nodeAnnouncement_2) } - def makeNewerChannelUpdate(channelUpdate: ChannelUpdate) : ChannelUpdate = { - val (priv, pub) = if (Announcements.isNode1(channelUpdate.channelFlags)) (priv1, priv2.publicKey) else (priv2, priv1.publicKey) - makeChannelUpdate(channelUpdate.chainHash, priv, pub, channelUpdate.shortChannelId, + def makeNewerChannelUpdate(channelAnnouncement: ChannelAnnouncement, channelUpdate: ChannelUpdate): ChannelUpdate = { + val (local, remote) = if (Announcements.isNode1(channelUpdate.channelFlags)) (channelAnnouncement.nodeId1, channelAnnouncement.nodeId2) else (channelAnnouncement.nodeId2, channelAnnouncement.nodeId1) + val priv = pub2priv(local) + makeChannelUpdate(channelUpdate.chainHash, priv, remote, channelUpdate.shortChannelId, channelUpdate.cltvExpiryDelta, channelUpdate.htlcMinimumMsat, channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, channelUpdate.htlcMinimumMsat, Announcements.isEnabled(channelUpdate.channelFlags), channelUpdate.timestamp + 5000) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala deleted file mode 100644 index f2733cdbbe..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncWithChecksumsSpec.scala +++ /dev/null @@ -1,163 +0,0 @@ -package fr.acinq.eclair.router - -import akka.actor.{Actor, ActorSystem, Props} -import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Satoshi, Script, Transaction, TxIn, TxOut} -import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult} -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.wire._ -import org.scalatest.FunSuiteLike - -import scala.collection.immutable.TreeMap -import scala.concurrent.duration._ - - -class RoutingSyncWithChecksumsSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { - - import RoutingSyncSpec.makeFakeRoutingInfo - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => - val desc = Router.getDesc(u, initChannels(u.shortChannelId)) - (desc) -> u - }.toMap - - class FakeWatcher extends Actor { - override def receive: Receive = { - case ValidateRequest(c) => - val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) - val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) - val fakeFundingTx = Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format - lockTime = 0) - sender ! ValidateResult(c, Right(fakeFundingTx, UtxoStatus.Unspent)) - } - } - - test("handle channel range extended (full sync)") { - val params = TestConstants.Alice.nodeParams - val watcher = system.actorOf(Props(new FakeWatcher())) - val router = TestFSMRef(new Router(params, watcher)) - val transport = TestProbe() - val sender = TestProbe() - sender.ignoreMsg { case _: TransportHandler.ReadAck => true } - val remoteNodeId = TestConstants.Bob.nodeParams.nodeId - - // ask router to send a channel range query - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS))) - val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, Some(optionExtendedQueryFlags)) = sender.expectMsgType[QueryChannelRange] - sender.expectMsgType[GossipTimestampFilter] - - // send back all our ids and timestamps - val block = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, - shortChannelIds = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.toList), - optionExtendedQueryFlags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), - extendedInfo_opt = Some(ExtendedInfo(shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, initChannelUpdates)))) - ) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block)) - - // router should ask for our first block of ids - val shortChannelIdAndFlags = block.shortChannelIds.array.map(shortChannelId => ShortChannelIdAndFlag(shortChannelId, (QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2).toByte)) - val shortChannelIdAndFlags1 = shortChannelIdAndFlags.take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags1.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags1.map(_.flag))))) - - // send the first 50 items - shortChannelIdAndFlags1.take(50).foreach(info => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(info.shortChannelId) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // send the last 50 items - shortChannelIdAndFlags1.drop(50).foreach(info => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(info.shortChannelId) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) - - // router should ask for our second block of ids - val shortChannelIdAndFlags2 = shortChannelIdAndFlags.drop(Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags2.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags2.map(_.flag))))) - - // send block #2 - shortChannelIdAndFlags2.foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) - - // router should ask for our third block of ids - val shortChannelIdAndFlags3 = shortChannelIdAndFlags.drop(2 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags3.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags3.map(_.flag))))) - - // send block #3 - shortChannelIdAndFlags3.foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) - - // router should ask for our fourth block of ids - val shortChannelIdAndFlags4 = shortChannelIdAndFlags.drop(3 * Router.SHORTID_WINDOW).take(Router.SHORTID_WINDOW) - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags4.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags4.map(_.flag))))) - - // send block #4 - shortChannelIdAndFlags4.foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id.shortChannelId) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) - - awaitCond({ - router.stateData.channels == initChannels - }, max = 30 seconds, interval = 500 millis) - - val updatedIds = shortChannelIds.drop(100).take(50) - val recentChannelUpdates = updatedIds.foldLeft(initChannelUpdates) { - case (updates, id) => - val desc = ChannelDesc(id, RoutingSyncSpec.priv1.publicKey, RoutingSyncSpec.priv2.publicKey) - val update = updates(desc) - val foo = Announcements.isNode1(update.channelFlags) - assert(foo) - val newUpdate = RoutingSyncSpec.makeNewerChannelUpdate(update) - updates.updated(desc, newUpdate) - } - // ask router to send a channel range query - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS))) - assert(sender.expectMsgType[QueryChannelRange].extendedQueryFlags_opt.isDefined) - sender.expectMsgType[GossipTimestampFilter] - - // send back all our ids and timestamps - val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, shortChannelIds.toList), - Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), - Some(ExtendedInfo(shortChannelIds.toList.map(Router.getChannelDigestInfo(initChannels, recentChannelUpdates))))) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) - - // router should ask for our new channel updates - val shortChannelIdAndFlags5 = block1.shortChannelIds.array.map(shortChannelId => ShortChannelIdAndFlag(shortChannelId, QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1)).filter(info => updatedIds.contains(info.shortChannelId)) - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, EncodedShortChannelIds(block.shortChannelIds.encoding, shortChannelIdAndFlags5.map(_.shortChannelId)), Some(EncodedQueryFlags(block.shortChannelIds.encoding, shortChannelIdAndFlags5.map(_.flag))))) - - } -} From faa9a8c6393060d3087a1559e7b7a542abcd167c Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 26 Mar 2019 19:12:44 +0100 Subject: [PATCH 60/86] tweak test --- eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f3d16487d7..71c1372afd 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 @@ -26,7 +26,7 @@ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements, SendPing} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router.{ChannelRangeQueriesSpec, Rebroadcast, RoutingSyncSpec} +import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec} import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingType, Error, Ping, Pong} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, randomBytes, wire} import org.scalatest.Outcome @@ -39,7 +39,7 @@ class PeerSpec extends TestkitBaseClass { val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) val channels = fakeRoutingInfo.map(_._1).toList val updates = (fakeRoutingInfo.map(_._2) ++ fakeRoutingInfo.map(_._3)).toList - val nodes = fakeRoutingInfo.toList.map(_._4) ++ fakeRoutingInfo.toList.map(_._5) + val nodes = (fakeRoutingInfo.map(_._4) ++ fakeRoutingInfo.map(_._5)).toList case class FixtureParam(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef) From 30336964f78a9eacb09225473e6ae654f66de7e2 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 14 Jun 2019 16:18:50 +0200 Subject: [PATCH 61/86] fixed ddcf2d418732e4023fbec4875d9e00b2c3382ec3 --- .../acinq/eclair/router/RoutingSyncSpec.scala | 2 +- .../wire/LightningMessageCodecsSpec.scala | 26 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 412b561c3c..928d9aeba3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -50,7 +50,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { class YesWatcher extends Actor { override def receive: Receive = { case ValidateRequest(c) => - val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2)))) + val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(c.bitcoinKey1, c.bitcoinKey2))) val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) val fakeFundingTx = Transaction( version = 2, 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 a68309e0e3..7f45127e96 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 @@ -285,7 +285,31 @@ class LightningMessageCodecsSpec extends FunSuite { case ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS => JString("TIMESTAMPS_AND_CHECKSUMS") })) - implicit val formats = org.json4s.DefaultFormats.withTypeHintFieldName("type") + new EncodingTypeSerializer + new ExtendedQueryFlagsSerializer + new ByteVectorSerializer + new ByteVector32Serializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer +new PaymentRequestSerializer + + implicit val formats = org.json4s.DefaultFormats.withTypeHintFieldName("type") + + new EncodingTypeSerializer + + new ExtendedQueryFlagsSerializer + + new ByteVectorSerializer + + new ByteVector32Serializer + + new UInt64Serializer + + new MilliSatoshiSerializer + + new ShortChannelIdSerializer + + new StateSerializer + + new ShaChainSerializer + + new PublicKeySerializer + + new PrivateKeySerializer + + new TransactionSerializer + + new TransactionWithInputInfoSerializer + + new InetSocketAddressSerializer + + new OutPointSerializer + + new OutPointKeySerializer + + new InputInfoSerializer + + new ColorSerializer + + new RouteResponseSerializer + + new ThrowableSerializer + + new FailureMessageSerializer + + new NodeAddressSerializer + + new DirectionSerializer + + new PaymentRequestSerializer + ShortTypeHints(List( classOf[QueryChannelRange], classOf[ReplyChannelRange], From a60048dbe2ff7e04a7f7eeb5dd75110ee160d7b0 Mon Sep 17 00:00:00 2001 From: araspitzu Date: Mon, 29 Jul 2019 10:32:05 +0200 Subject: [PATCH 62/86] Update list of commands in eclair-cli help (#1091) * Add missing API endpoints to eclair-cli help --- eclair-core/eclair-cli | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 08984c7ab4..89a05cdde7 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -27,11 +27,12 @@ where OPTIONS can be: -s Some commands can print a trimmed JSON and COMMAND is one of: - getinfo, connect, open, close, forceclose, updaterelayfee, + getinfo, connect, disconnect, open, close, forceclose, updaterelayfee, peers, channels, channel, allnodes, allchannels, allupdates findroute, findroutetonode, parseinvoice, payinvoice, sendtonode, - getsentinfo, createinvoice, getinvoice, listinvoices, - listpendinginvoices, getreceivedinfo, audit, networkfees, channelstats + sendtoroute, getsentinfo, createinvoice, getinvoice, listinvoices, + listpendinginvoices, getreceivedinfo, audit, networkfees, + channelstats, usablebalances Examples -------- From 7582402379893d2915a9ee048f7440397210aed8 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 31 Jul 2019 13:35:26 +0000 Subject: [PATCH 63/86] Documentation update (#1092) * Fix README style warnings * Add documentation links to the wiki * Update build instructions. --- BUILD.md | 52 +++++++++++++++++++++++++------------ README.md | 77 +++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 84 insertions(+), 45 deletions(-) diff --git a/BUILD.md b/BUILD.md index 0c050418eb..054c10e8db 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,39 +1,57 @@ # Building Eclair ## Requirements -- [OpenJDK 11](https://jdk.java.net/11/). + +- [OpenJDK 11](https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot). - [Maven](https://maven.apache.org/download.cgi) 3.6.0 or newer - [Docker](https://www.docker.com/) 18.03 or newer (optional) if you want to run all tests -:warning: You can also use [Oracle JDK 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) to build and run eclair, but we recommend you use Open JDK11. +:warning: You can also use [Oracle JDK 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) to build and run eclair, but we recommend you use OpenJDK 11. ## Build -To build the project, simply run: + +To build the project and run the tests, simply run: + ```shell -$ mvn install +mvn install ``` -#### Other build options +### Other build options To skip all tests, run: + +```shell +mvn install -DskipTests +``` + +To only build the `eclair-node` module, run: + ```shell -$ mvn install -DskipTests +mvn install -pl eclair-node -am -DskipTests ``` -To only build the `eclair-node` module + +To run the tests, run: + +```shell +mvn test +``` + +To run tests for a specific class, run: + ```shell -$ mvn install -pl eclair-node -am -DskipTests +mvn test -Dsuites=* ``` -# Building the API documentation +## Build the API documentation -## Slate +### Slate The API doc is generated via slate and hosted on github pages. To make a change and update the doc follow the steps: -1. git checkout slate-doc -2. Install your local dependencies for slate, more info [here](https://github.com/lord/slate#getting-started-with-slate) -3. Edit `source/index.html.md` and save your changes. -4. Commit all the changes to git, before deploying the repo should be clean. -5. Push your commit to remote. -6. Run `./deploy.sh` -7. Wait a few minutes and the doc should be updated at https://acinq.github.io/eclair \ No newline at end of file +1. `git checkout slate-doc` +2. Install your local dependencies for slate, more info [here](https://github.com/lord/slate#getting-started-with-slate) +3. Edit `source/index.html.md` and save your changes. +4. Commit all the changes to git, before deploying the repo should be clean. +5. Push your commit to remote. +6. Run `./deploy.sh` +7. Wait a few minutes and the doc should be updated at [https://acinq.github.io/eclair](https://acinq.github.io/eclair) diff --git a/README.md b/README.md index cf308835b6..ae9abf156f 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,20 @@ **Eclair** (French for Lightning) is a Scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON API is also available. This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning) and [lnd](https://github.com/LightningNetwork/lnd). - - --- - - :construction: Both the BOLTs and Eclair itself are still a work in progress. Expect things to break/change! - - :rotating_light: If you run Eclair on mainnet (which is the default setting): - - Keep in mind that it is beta-quality software and **don't put too much money** in it - - Eclair's JSON API should **NOT** be accessible from the outside world (similarly to Bitcoin Core API) - + +--- + +:construction: Both the BOLTs and Eclair itself are still a work in progress. Expect things to break/change! + +:rotating_light: If you run Eclair on mainnet (which is the default setting): + +* Keep in mind that it is beta-quality software and **don't put too much money** in it +* Eclair's JSON API should **NOT** be accessible from the outside world (similarly to Bitcoin Core API) + --- ## Lightning Network Specification Compliance + Please see the latest [release note](https://github.com/ACINQ/eclair/releases) for detailed information on BOLT compliance. ## Overview @@ -32,18 +34,26 @@ Eclair offers a feature rich HTTP API that enables application developers to eas For more information please visit the [API documentation website](https://acinq.github.io/eclair). +## Documentation + +Please visit our [wiki](https://github.com/acinq/eclair/wiki) to find detailed instructions on how to configure your +node, connect to other nodes, open channels, send and receive payments and more advanced scenario. + +You will find detailed guides and frequently asked questions there. + ## Installation ### Configuring Bitcoin Core :warning: Eclair requires Bitcoin Core 0.17.1 or higher. If you are upgrading an existing wallet, you need to create a new address and send all your funds to that address. -Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node. +Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node. Eclair will use any BTC it finds in the Bitcoin Core wallet to fund any channels you choose to open. Eclair will return BTC from closed channels to this wallet. You can configure your Bitcoin Node to use either `p2sh-segwit` addresses or `bech32` addresses, Eclair is compatible with both modes. Run bitcoind with the following minimal `bitcoin.conf`: -``` + +```conf server=1 rpcuser=foo rpcpassword=bar @@ -55,17 +65,22 @@ zmqpubrawtx=tcp://127.0.0.1:29000 ### Installing Eclair Eclair is developed in [Scala](https://www.scala-lang.org/), a powerful functional language that runs on the JVM, and is packaged as a JAR (Java Archive) file. We provide 2 different packages, which internally use the same core libraries: + * eclair-node, which is a headless application that you can run on servers and desktops, and control from the command line * eclair-node-gui, which also includes a JavaFX GUI To run Eclair, you first need to install Java, we recommend that you use [OpenJDK 11](https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot). Eclair will also run on Oracle JDK 1.8, Oracle JDK 11, and other versions of OpenJDK but we don't recommend using them. Then download our latest [release](https://github.com/ACINQ/eclair/releases) and depending on whether or not you want a GUI run the following command: + * with GUI: + ```shell java -jar eclair-node-gui--.jar ``` + * without GUI: + ```shell java -jar eclair-node--.jar ``` @@ -78,7 +93,7 @@ Eclair reads its configuration file, and write its logs, to `~/.eclair` by defau To change your node's configuration, create a file named `eclair.conf` in `~/.eclair`. Here's an example configuration file: -``` +```conf eclair.node-alias=eclair eclair.node-color=49daaa ``` @@ -111,10 +126,11 @@ Some advanced parameters can be changed with java environment variables. Most us name | description | default value ----------------------|--------------------------------------------|-------------- eclair.datadir | Path to the data directory | ~/.eclair -eclair.headless | Run eclair without a GUI | +eclair.headless | Run eclair without a GUI | eclair.printToConsole | Log to stdout (in addition to eclair.log) | For example, to specify a different data directory you would run the following command: + ```shell java -Declair.datadir=/tmp/node1 -jar eclair-node-gui--.jar ``` @@ -130,41 +146,44 @@ java -Dlogback.configurationFile=/path/to/logback-custom.xml -jar eclair-node-gu #### Backup The files that you need to backup are located in your data directory. You must backup: -- your seed (`seed.dat`) -- your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) + +* your seed (`seed.dat`) +* your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) Your seed never changes once it has been created, but your channels will change whenever you receive or send payments. Eclair will -create and maintain a snapshot of its database, named `eclair.sqlite.bak`, in your data directory, and update it when needed. This file is +create and maintain a snapshot of its database, named `eclair.sqlite.bak`, in your data directory, and update it when needed. This file is always consistent and safe to use even when Eclair is running, and this is what you should backup regularly. For example you could configure a `cron` task for your backup job. Or you could configure an optional notification script to be called by eclair once a new database snapshot has been created, using the following option: -``` + +```conf eclair.backup-notify-script = "/absolute/path/to/script.sh" ``` + Make sure that your script is executable and uses an absolute path name for `eclair.sqlite.bak`. -Note that depending on your filesystem, in your backup process we recommend first moving `eclair.sqlite.bak` to some temporary file +Note that depending on your filesystem, in your backup process we recommend first moving `eclair.sqlite.bak` to some temporary file before copying that file to your final backup location. - ## Docker A [Dockerfile](Dockerfile) image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node. You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`. -``` +```shell docker run -ti --rm -e "JAVA_OPTS=-Xmx512m -Declair.api.binding-ip=0.0.0.0 -Declair.node-alias=node-pm -Declair.printToConsole" acinq/eclair ``` If you want to persist the data directory, you can make the volume to your host with the `-v` argument, as the following example: -``` +```shell docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq/eclair ``` If you enabled the API you can check the status of eclair using the command line tool: -``` + +```shell docker exec eclair-cli -p foobar getinfo ``` @@ -175,6 +194,7 @@ For advanced usage, Eclair supports plugins written in Scala, Java, or any JVM-c A valid plugin is a jar that contains an implementation of the [Plugin](eclair-node/src/main/scala/fr/acinq/eclair/Plugin.scala) interface. Here is how to run Eclair with plugins: + ```shell java -jar eclair-node--.jar <...> ``` @@ -184,15 +204,15 @@ java -jar eclair-node--.jar <... Eclair is configured to run on mainnet by default, but you can still run it on testnet (or regtest): start your Bitcoin Node in testnet mode (add `testnet=1` in `bitcoin.conf` or start with `-testnet`), and change Eclair's chain parameter and Bitcoin RPC port: -``` +```conf eclair.chain=testnet eclair.bitcoind.rpcport=18332 ``` -You may also want to take advantage of the new configuration sections in `bitcoin.conf` to manage parameters that are network specific, +You may also want to take advantage of the new configuration sections in `bitcoin.conf` to manage parameters that are network specific, so you can easily run your bitcoin node on both mainnet and testnet. For example you could use: -``` +```conf server=1 txindex=1 [main] @@ -208,6 +228,7 @@ zmqpubrawtx=tcp://127.0.0.1:29001 ``` ## Resources -- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja -- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell -- [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to + +* [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja +* [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell +* [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to From f53b77000127a1f3cd69d27a0240a21f153af726 Mon Sep 17 00:00:00 2001 From: araspitzu Date: Wed, 7 Aug 2019 17:37:38 +0200 Subject: [PATCH 64/86] Typed amounts (#1088) * Type all amounts used in eclair * Add eclair.MilliSatoshi class * Use bitcoin-lib 0.14 * Add specialized codecs for Satoshi/MilliSatoshi * Rename 'toSatoshi' to 'truncateToSatoshi' to highlight it's a precision-losing conversion --- eclair-core/eclair-cli | 2 +- .../scala/fr/acinq/eclair/CoinUtils.scala | 16 +- .../main/scala/fr/acinq/eclair/Eclair.scala | 42 +- .../scala/fr/acinq/eclair/NodeParams.scala | 25 +- .../fr/acinq/eclair/api/ExtraDirectives.scala | 5 +- .../eclair/api/FormParamExtractors.scala | 14 +- .../fr/acinq/eclair/api/JsonSerializers.scala | 9 +- .../scala/fr/acinq/eclair/api/Service.scala | 16 +- .../fr/acinq/eclair/channel/Channel.scala | 68 +-- .../acinq/eclair/channel/ChannelEvents.scala | 5 +- .../eclair/channel/ChannelExceptions.scala | 28 +- .../acinq/eclair/channel/ChannelTypes.scala | 25 +- .../fr/acinq/eclair/channel/Commitments.scala | 82 ++-- .../fr/acinq/eclair/channel/Helpers.scala | 70 +-- .../scala/fr/acinq/eclair/db/AuditDb.scala | 8 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 10 +- .../eclair/db/sqlite/SqliteAuditDb.scala | 17 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 15 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 27 +- .../main/scala/fr/acinq/eclair/package.scala | 52 ++- .../fr/acinq/eclair/payment/Auditor.scala | 12 +- .../fr/acinq/eclair/payment/Autoprobe.scala | 6 +- .../eclair/payment/LocalPaymentHandler.scala | 14 +- .../acinq/eclair/payment/PaymentEvents.scala | 5 +- .../eclair/payment/PaymentLifecycle.scala | 50 +- .../acinq/eclair/payment/PaymentRequest.scala | 5 +- .../fr/acinq/eclair/payment/Relayer.scala | 24 +- .../acinq/eclair/router/Announcements.scala | 6 +- .../scala/fr/acinq/eclair/router/Graph.scala | 36 +- .../scala/fr/acinq/eclair/router/Router.scala | 22 +- .../eclair/transactions/CommitmentSpec.scala | 17 +- .../eclair/transactions/Transactions.scala | 31 +- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 20 +- .../fr/acinq/eclair/wire/CommonCodecs.scala | 8 +- .../fr/acinq/eclair/wire/FailureMessage.scala | 19 +- .../eclair/wire/LightningMessageCodecs.scala | 34 +- .../eclair/wire/LightningMessageTypes.scala | 30 +- .../scala/fr/acinq/eclair/wire/Onion.scala | 6 +- .../src/test/resources/api/usablebalances | 2 +- .../scenarii/01-offer1.script.expected | 24 +- .../scenarii/02-offer2.script.expected | 24 +- .../scenarii/03-fulfill1.script.expected | 16 +- .../04-two-commits-onedir.script.expected | 16 +- .../10-offers-crossover.script.expected | 32 +- .../11-commits-crossover.script.expected | 32 +- .../scala/fr/acinq/eclair/CoinUtilsSpec.scala | 2 +- .../fr/acinq/eclair/EclairImplSpec.scala | 52 +-- .../scala/fr/acinq/eclair/TestConstants.scala | 30 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 17 +- .../eclair/api/JsonSerializersSpec.scala | 2 +- .../bitcoind/BitcoinCoreWalletSpec.scala | 2 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 4 +- .../acinq/eclair/channel/ThroughputSpec.scala | 2 +- .../states/StateTestsHelperMethods.scala | 10 +- .../a/WaitForAcceptChannelStateSpec.scala | 10 +- .../a/WaitForOpenChannelStateSpec.scala | 28 +- .../b/WaitForFundingCreatedStateSpec.scala | 14 +- .../channel/states/e/NormalStateSpec.scala | 303 +++++++------ .../channel/states/e/OfflineStateSpec.scala | 26 +- .../channel/states/f/ShutdownStateSpec.scala | 10 +- .../states/g/NegotiatingStateSpec.scala | 14 +- .../channel/states/h/ClosingStateSpec.scala | 22 +- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 14 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 10 +- .../eclair/db/SqlitePaymentsDbSpec.scala | 28 +- .../eclair/integration/IntegrationSpec.scala | 53 +-- .../interop/rustytests/RustyTestsSpec.scala | 6 +- .../rustytests/SynchronizationPipe.scala | 12 +- .../fr/acinq/eclair/io/HtlcReaperSpec.scala | 7 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- .../eclair/payment/ChannelSelectionSpec.scala | 40 +- .../eclair/payment/HtlcGenerationSpec.scala | 29 +- .../eclair/payment/PaymentHandlerSpec.scala | 12 +- .../eclair/payment/PaymentLifecycleSpec.scala | 18 +- .../eclair/payment/PaymentRequestSpec.scala | 21 +- .../fr/acinq/eclair/payment/RelayerSpec.scala | 72 +-- .../AnnouncementsBatchValidationSpec.scala | 5 +- .../eclair/router/AnnouncementsSpec.scala | 12 +- .../acinq/eclair/router/BaseRouterSpec.scala | 16 +- .../router/ChannelRangeQueriesSpec.scala | 10 +- .../fr/acinq/eclair/router/GraphSpec.scala | 66 +-- .../eclair/router/RouteCalculationSpec.scala | 428 +++++++++--------- .../fr/acinq/eclair/router/RouterSpec.scala | 16 +- .../acinq/eclair/router/RoutingSyncSpec.scala | 4 +- .../transactions/CommitmentSpecSpec.scala | 30 +- .../eclair/transactions/TestVectorsSpec.scala | 49 +- .../transactions/TransactionsSpec.scala | 43 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 84 ++-- .../wire/FailureMessageCodecsSpec.scala | 14 +- .../wire/LightningMessageCodecsSpec.scala | 14 +- .../acinq/eclair/wire/OnionCodecsSpec.scala | 4 +- .../fr/acinq/eclair/gui/GUIUpdater.scala | 8 +- .../scala/fr/acinq/eclair/gui/Handlers.scala | 6 +- .../controllers/ChannelPaneController.scala | 8 +- .../gui/controllers/MainController.scala | 4 +- .../controllers/OpenChannelController.scala | 15 +- .../ReceivePaymentController.scala | 2 +- pom.xml | 2 +- 98 files changed, 1425 insertions(+), 1314 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 89a05cdde7..be2102d71c 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -89,7 +89,7 @@ jq_filter='if type=="object" and .error != null then .error else .'; # apply special jq filter if we are in "short" ouput mode -- only for specific commands such as 'channels' if [ "$short" = true ]; then - jq_channel_filter="{ nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }"; + jq_channel_filter="{ nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocal / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }"; case $api_endpoint in "channels") jq_filter="$jq_filter | map( $jq_channel_filter )" ;; "channel") jq_filter="$jq_filter | $jq_channel_filter" ;; diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala index e4453ab8aa..2e58bef4dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala @@ -17,10 +17,8 @@ package fr.acinq.eclair import java.text.{DecimalFormat, NumberFormat} - -import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, Satoshi} import grizzled.slf4j.Logging - import scala.util.{Failure, Success, Try} /** @@ -154,7 +152,7 @@ object CoinUtils extends Logging { } def convertStringAmountToSat(amount: String, unit: String): Satoshi = - fr.acinq.bitcoin.millisatoshi2satoshi(CoinUtils.convertStringAmountToMsat(amount, unit)) + CoinUtils.convertStringAmountToMsat(amount, unit).truncateToSatoshi /** * Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported. @@ -223,6 +221,11 @@ object CoinUtils extends Logging { if (withUnit) s"$formatted ${unit.shortLabel}" else formatted } + def formatAmountInUnit(amount: MilliSatoshi, unit: CoinUnit, withUnit: Boolean): String = { + val formatted = COIN_FORMAT.format(rawAmountInUnit(amount, unit)) + if (withUnit) s"$formatted ${unit.shortLabel}" else formatted + } + /** * Converts the amount to the user preferred unit and returns the BigDecimal value. * This method is useful to feed numeric text input without formatting. @@ -237,7 +240,10 @@ object CoinUtils extends Logging { case a => throw new IllegalArgumentException(s"unhandled unit $a") }) match { case Success(b) => b - case Failure(t) => logger.error("can not convert amount to user unit", t) + case Failure(t) => + logger.error("can not convert amount to user unit", t) -1 } + + def rawAmountInUnit(msat: MilliSatoshi, unit: CoinUnit): BigDecimal = BigDecimal(msat.amount) / unit.factorToMsat } 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 e423806715..e22b621d35 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -22,7 +22,7 @@ import akka.actor.ActorRef import akka.pattern._ import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats} @@ -60,13 +60,13 @@ trait Eclair { def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] - def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat_opt: Option[Long], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] + def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey_opt: Option[ByteVector])(implicit timeout: Timeout): Future[String] def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[String] - def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] + def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBase: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] @@ -74,17 +74,17 @@ trait Eclair { def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] - def receive(description: String, amountMsat_opt: Option[Long], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(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 receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]] - def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Long] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID] + def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID] def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] - def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] - def sendToRoute(route: Seq[PublicKey], amountMsat: Long, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] + def sendToRoute(route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] @@ -122,13 +122,13 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String] } - override def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat_opt: Option[Long], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] = { + override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds)) (appKit.switchboard ? Peer.OpenChannel( remoteNodeId = nodeId, - fundingSatoshis = Satoshi(fundingSatoshis), - pushMsat = pushMsat_opt.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), + fundingSatoshis = fundingAmount, + pushMsat = pushAmount_opt.getOrElse(MilliSatoshi(0)), fundingTxFeeratePerKw_opt = fundingFeerateSatByte_opt.map(feerateByte2Kw), channelFlags = flags_opt.map(_.toByte), timeout_opt = Some(openTimeout))).mapTo[String] @@ -142,7 +142,7 @@ class EclairImpl(appKit: Kit) extends Eclair { sendToChannel(channelIdentifier, CMD_FORCECLOSE).mapTo[String] } - override def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] = { + override def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] = { sendToChannel(channelIdentifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String] } @@ -177,31 +177,31 @@ class EclairImpl(appKit: Kit) extends Eclair { case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) } - override def receive(description: String, amountMsat_opt: Option[Long], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(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(description = description, amountMsat_opt = amountMsat_opt.map(MilliSatoshi), expirySeconds_opt = expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest] + (appKit.paymentHandler ? ReceivePayment(description = description, amount_opt = amount_opt, expirySeconds_opt = expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest] } - override def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = { - (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse] + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = { + (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse] } - override def sendToRoute(route: Seq[PublicKey], amountMsat: Long, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] = { - (appKit.paymentInitiator ? SendPaymentToRoute(amountMsat, paymentHash, route, finalCltvExpiry)).mapTo[UUID] + override def sendToRoute(route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] = { + (appKit.paymentInitiator ? SendPaymentToRoute(amount, paymentHash, route, finalCltvExpiry)).mapTo[UUID] } - override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThresholdSat_opt: Option[Long], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = { + override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = { val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts) val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf) val routeParams = defaultRouteParams.copy( maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct), - maxFeeBaseMsat = feeThresholdSat_opt.map(_ * 1000).getOrElse(defaultRouteParams.maxFeeBaseMsat) + maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase) ) val sendPayment = minFinalCltvExpiry_opt match { - case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams)) - case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams)) + case Some(minCltv) => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams)) + case None => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams)) } (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } 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 55f5fb2859..2daa66510f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -23,8 +23,8 @@ import java.util.concurrent.TimeUnit import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32} -import fr.acinq.eclair.NodeParams.{WatcherType} +import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} +import fr.acinq.eclair.NodeParams.WatcherType import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, OnChainFeeConf} import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.KeyManager @@ -33,7 +33,6 @@ import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector - import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -47,17 +46,17 @@ case class NodeParams(keyManager: KeyManager, globalFeatures: ByteVector, localFeatures: ByteVector, overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)], - dustLimitSatoshis: Long, + dustLimit: Satoshi, onChainFeeConf: OnChainFeeConf, maxHtlcValueInFlightMsat: UInt64, maxAcceptedHtlcs: Int, expiryDeltaBlocks: Int, fulfillSafetyBeforeTimeoutBlocks: Int, - htlcMinimumMsat: Int, + htlcMinimum: MilliSatoshi, toRemoteDelayBlocks: Int, maxToLocalDelayBlocks: Int, minDepthBlocks: Int, - feeBaseMsat: Int, + feeBase: MilliSatoshi, feeProportionalMillionth: Int, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double, @@ -73,7 +72,7 @@ case class NodeParams(keyManager: KeyManager, channelFlags: Byte, watcherType: WatcherType, paymentRequestExpiry: FiniteDuration, - minFundingSatoshis: Long, + minFundingSatoshis: Satoshi, routerConf: RouterConf, socksProxy_opt: Option[Socks5ProxyParams], maxPaymentAttempts: Int) { @@ -137,7 +136,7 @@ object NodeParams { case _ => BITCOIND } - val dustLimitSatoshis = config.getLong("dust-limit-satoshis") + val dustLimitSatoshis = Satoshi(config.getLong("dust-limit-satoshis")) if (chainHash == Block.LivenetGenesisBlock.hash) { require(dustLimitSatoshis >= Channel.MIN_DUSTLIMIT, s"dust limit must be greater than ${Channel.MIN_DUSTLIMIT}") } @@ -195,7 +194,7 @@ object NodeParams { globalFeatures = ByteVector.fromValidHex(config.getString("global-features")), localFeatures = ByteVector.fromValidHex(config.getString("local-features")), overrideFeatures = overrideFeatures, - dustLimitSatoshis = dustLimitSatoshis, + dustLimit = dustLimitSatoshis, onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, feeEstimator = feeEstimator, @@ -206,11 +205,11 @@ object NodeParams { maxAcceptedHtlcs = maxAcceptedHtlcs, expiryDeltaBlocks = expiryDeltaBlocks, fulfillSafetyBeforeTimeoutBlocks = fulfillSafetyBeforeTimeoutBlocks, - htlcMinimumMsat = config.getInt("htlc-minimum-msat"), + htlcMinimum = MilliSatoshi(config.getInt("htlc-minimum-msat")), toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"), maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"), minDepthBlocks = config.getInt("mindepth-blocks"), - feeBaseMsat = config.getInt("fee-base-msat"), + feeBase = MilliSatoshi(config.getInt("fee-base-msat")), feeProportionalMillionth = config.getInt("fee-proportional-millionths"), reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"), @@ -226,14 +225,14 @@ object NodeParams { channelFlags = config.getInt("channel-flags").toByte, watcherType = watcherType, paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), - minFundingSatoshis = config.getLong("min-funding-satoshis"), + minFundingSatoshis = Satoshi(config.getLong("min-funding-satoshis")), routerConf = RouterConf( channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"), searchMaxRouteLength = config.getInt("router.path-finding.max-route-length"), searchMaxCltv = config.getInt("router.path-finding.max-cltv"), - searchMaxFeeBaseSat = config.getLong("router.path-finding.fee-threshold-sat"), + searchMaxFeeBase = Satoshi(config.getLong("router.path-finding.fee-threshold-sat")), searchMaxFeePct = config.getDouble("router.path-finding.max-fee-pct"), searchHeuristicsEnabled = config.getBoolean("router.path-finding.heuristics-enable"), searchRatioCltv = config.getDouble("router.path-finding.ratio-cltv"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala index 9d5e1414f0..2973b53ec6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala @@ -22,10 +22,11 @@ import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.server.{Directive1, Directives, MalformedFormFieldRejection, Route} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.api.FormParamExtractors.{sha256HashUnmarshaller, shortChannelIdUnmarshaller} import fr.acinq.eclair.api.JsonSupport._ import fr.acinq.eclair.payment.PaymentRequest + import scala.concurrent.Future import scala.util.{Failure, Success} @@ -38,7 +39,7 @@ trait ExtraDirectives extends Directives { val paymentHashFormParam = "paymentHash".as[ByteVector32](sha256HashUnmarshaller) val fromFormParam = "from".as[Long] val toFormParam = "to".as[Long] - val amountMsatFormParam = "amountMsat".as[Long] + val amountMsatFormParam = "amountMsat".as[MilliSatoshi] val invoiceFormParam = "invoice".as[PaymentRequest] // custom directive to fail with HTTP 404 (and JSON response) if the element was not found diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala index 17425cf368..cbe045faa8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -21,12 +21,13 @@ import java.util.UUID import JsonSupport._ import akka.http.scaladsl.unmarshalling.Unmarshaller import akka.util.Timeout -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.PaymentRequest import scodec.bits.ByteVector + import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} @@ -75,4 +76,13 @@ object FormParamExtractors { } } + implicit val satoshiUnmarshaller: Unmarshaller[String, Satoshi] = Unmarshaller.strict { str => + Satoshi(str.toLong) + } + + implicit val millisatoshiUnmarshaller: Unmarshaller[String, MilliSatoshi] = Unmarshaller.strict { str => + MilliSatoshi(str.toLong) + } + + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index a1a586941f..203c6c7f35 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -23,7 +23,7 @@ import com.google.common.net.HostAndPort import de.heikoseeberger.akkahttpjson4s.Json4sSupport import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, MilliSatoshi, OutPoint, Transaction} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.channel.{ChannelVersion, State} import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.db.OutgoingPaymentStatus @@ -32,7 +32,7 @@ import fr.acinq.eclair.router.RouteResponse import fr.acinq.eclair.transactions.Direction import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64} import org.json4s.JsonAST._ import org.json4s.{CustomKeySerializer, CustomSerializer, TypeHints, jackson} import scodec.bits.ByteVector @@ -57,6 +57,10 @@ class UInt64Serializer extends CustomSerializer[UInt64](format => ({ null }, { case x: UInt64 => JInt(x.toBigInt) })) +class SatoshiSerializer extends CustomSerializer[Satoshi](format => ({ null }, { + case x: Satoshi => JInt(x.amount) +})) + class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ({ null }, { case x: MilliSatoshi => JInt(x.amount) })) @@ -187,6 +191,7 @@ object JsonSupport extends Json4sSupport { new ByteVector32Serializer + new ByteVector64Serializer + new UInt64Serializer + + new SatoshiSerializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 3d781cadc5..7d50d75217 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -31,14 +31,14 @@ import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.util.Timeout import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.api.FormParamExtractors._ import fr.acinq.eclair.api.JsonSupport.CustomTypeHints import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed import fr.acinq.eclair.payment.{PaymentReceived, PaymentRequest, _} -import fr.acinq.eclair.{Eclair, ShortChannelId} +import fr.acinq.eclair.{Eclair, MilliSatoshi, ShortChannelId} import grizzled.slf4j.Logging import org.json4s.jackson.Serialization import scodec.bits.ByteVector @@ -156,14 +156,14 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("open") { - formFields(nodeIdFormParam, "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) { + formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) { (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt) => complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt)) } } ~ path("updaterelayfee") { withChannelIdentifier { channelIdentifier => - formFields("feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => + formFields("feeBaseMsat".as[MilliSatoshi], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => complete(eclairApi.updateRelayFee(channelIdentifier, feeBase, feeProportional)) } } @@ -206,7 +206,7 @@ trait Service extends ExtraDirectives with Logging { } ~ path("findroute") { formFields(invoiceFormParam, amountMsatFormParam.?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount, invoice.routingInfo)) case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) } @@ -222,16 +222,16 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("payinvoice") { - formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Long].?, "maxFeePct".as[Double].?) { + formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt) => - complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + complete(eclairApi.send(nodeId, amount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt) => complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) } } ~ path("sendtonode") { - formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Long].?, "maxFeePct".as[Double].?) { (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt) => + formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?) { (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt) => complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) } } ~ 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 72585ee2fd..d095f595fb 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 @@ -20,7 +20,7 @@ import akka.actor.{ActorRef, FSM, OneForOneStrategy, Props, Status, SupervisorSt import akka.event.Logging.MDC import akka.pattern.pipe import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, ScriptFlags, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Helpers.{Closing, Funding} @@ -48,11 +48,11 @@ object Channel { val ANNOUNCEMENTS_MINCONF = 6 // https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements - val MAX_FUNDING_SATOSHIS = 16777216L // = 2^24 + val MAX_FUNDING = Satoshi(16777216L) // = 2^24 val MAX_ACCEPTED_HTLCS = 483 // we don't want the counterparty to use a dust limit lower than that, because they wouldn't only hurt themselves we may need them to publish their commit tx in certain cases (backup/restore) - val MIN_DUSTLIMIT = 546 + val MIN_DUSTLIMIT = Satoshi(546) // we won't exchange more than this many signatures when negotiating the closing fee val MAX_NEGOTIATION_ITERATIONS = 20 @@ -152,10 +152,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId temporaryChannelId = temporaryChannelId, fundingSatoshis = fundingSatoshis, pushMsat = pushMsat, - dustLimitSatoshis = localParams.dustLimitSatoshis, + dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserveSatoshis, - htlcMinimumMsat = localParams.htlcMinimumMsat, + channelReserveSatoshis = localParams.channelReserve, + htlcMinimumMsat = localParams.htlcMinimum, feeratePerKw = initialFeeratePerKw, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, @@ -227,7 +227,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down val candidateChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, normal.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, - normal.commitments.remoteParams.htlcMinimumMsat, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, normal.commitments.localCommit.spec.totalFunds, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) + normal.commitments.remoteParams.htlcMinimum, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, normal.commitments.localCommit.spec.totalFunds, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) val channelUpdate1 = if (Announcements.areSame(candidateChannelUpdate, normal.channelUpdate)) { // if there was no configuration change we keep the existing channel update normal.channelUpdate @@ -274,11 +274,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, - dustLimitSatoshis = localParams.dustLimitSatoshis, + dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserveSatoshis, + channelReserveSatoshis = localParams.channelReserve, minimumDepth = minimumDepth, - htlcMinimumMsat = localParams.htlcMinimumMsat, + htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, @@ -289,10 +289,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0)) val remoteParams = RemoteParams( nodeId = remoteNodeId, - dustLimitSatoshis = open.dustLimitSatoshis, + dustLimit = open.dustLimitSatoshis, maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - channelReserveSatoshis = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimumMsat = open.htlcMinimumMsat, + channelReserve = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + htlcMinimum = open.htlcMinimumMsat, toSelfDelay = open.toSelfDelay, maxAcceptedHtlcs = open.maxAcceptedHtlcs, fundingPubKey = open.fundingPubkey, @@ -322,10 +322,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // TODO: check equality of temporaryChannelId? or should be done upstream val remoteParams = RemoteParams( nodeId = remoteNodeId, - dustLimitSatoshis = accept.dustLimitSatoshis, + dustLimit = accept.dustLimitSatoshis, maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - channelReserveSatoshis = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimumMsat = accept.htlcMinimumMsat, + channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + htlcMinimum = accept.htlcMinimumMsat, toSelfDelay = accept.toSelfDelay, maxAcceptedHtlcs = accept.maxAcceptedHtlcs, fundingPubKey = accept.fundingPubkey, @@ -338,7 +338,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.debug(s"remote params: $remoteParams") val localFundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, Satoshi(fundingSatoshis), fundingTxFeeratePerKw).pipeTo(self) + wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open) } @@ -360,9 +360,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) => + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) => // let's create the first commitment tx that spends the yet uncommitted funding tx - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) // signature of their initial commitment tx that pays remote pushMsat @@ -401,9 +401,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, _)) => + case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, _)) => // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) // check remote signature validity val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) @@ -561,7 +561,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimum, nodeParams.feeBase, nodeParams.feeProportionalMillionth, commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.schedule(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, interval = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None) storing() @@ -704,7 +704,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) ++ Transactions.trimReceivedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) + val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec) ++ Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec) trimmedHtlcs collect { case DirectedHtlc(_, u) => log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") @@ -716,9 +716,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId self ! BroadcastChannelUpdate(AboveReserve) } context.system.eventStream.publish(ChannelSignatureSent(self, commitments1)) - if (nextRemoteCommit.spec.toRemoteMsat != d.commitments.remoteCommit.spec.toRemoteMsat) { + if (nextRemoteCommit.spec.toRemote != d.commitments.remoteCommit.spec.toRemote) { // we send this event only when our balance changes (note that remoteCommit.toRemote == toLocal) - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, nextRemoteCommit.spec.toRemoteMsat, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, nextRemoteCommit.spec.toRemote, commitments1)) } // we expect a quick response from our peer setTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer = context.parent), timeout = nodeParams.revocationTimeout, repeat = false) @@ -1159,21 +1159,21 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(NEGOTIATING)(handleExceptions { case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig), d: DATA_NEGOTIATING) => log.info(s"received closingFeeSatoshis=$remoteClosingFee") - Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(remoteClosingFee), remoteSig) match { + Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { case Success(signedClosingTx) if d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).contains(remoteClosingFee) || d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS => // we close when we converge or when there were too many iterations handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) case Success(signedClosingTx) => // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee - val lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).map(Satoshi) + val lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis) val nextClosingFee = Closing.nextClosingFee( localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)), - remoteClosingFee = Satoshi(remoteClosingFee)) + remoteClosingFee = remoteClosingFee) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) if (lastLocalClosingFee.contains(nextClosingFee)) { // next computed fee is the same than the one we previously sent (probably because of rounding), let's close now handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) - } else if (nextClosingFee == Satoshi(remoteClosingFee)) { + } else if (nextClosingFee == remoteClosingFee) { // we have converged! val closingTxProposed1 = d.closingTxProposed match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx.tx, closingSigned)) @@ -1310,9 +1310,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedoutHtlcs = - Closing.timedoutHtlcs(d.commitments.localCommit, Satoshi(d.commitments.localParams.dustLimitSatoshis), tx) ++ - Closing.timedoutHtlcs(d.commitments.remoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx) ++ - d.commitments.remoteNextCommitInfo.left.toSeq.flatMap(r => Closing.timedoutHtlcs(r.nextRemoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx)) + Closing.timedoutHtlcs(d.commitments.localCommit, d.commitments.localParams.dustLimit, tx) ++ + Closing.timedoutHtlcs(d.commitments.remoteCommit, d.commitments.remoteParams.dustLimit, tx) ++ + d.commitments.remoteNextCommitInfo.left.toSeq.flatMap(r => Closing.timedoutHtlcs(r.nextRemoteCommit, d.commitments.remoteParams.dustLimit, tx)) timedoutHtlcs.foreach { add => d.commitments.originChannels.get(add.id) match { case Some(origin) => @@ -1339,7 +1339,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Closing .onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) .map(add => (add, d.commitments.originChannels.get(add.id).collect { case Local(id, _) => id })) // we resolve the payment id if this was a local payment - .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = MilliSatoshi(add.amountMsat), add.paymentHash)) } + .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = add.amountMsat, add.paymentHash)) } // we update the channel data val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) // and we also send events related to fee @@ -2207,7 +2207,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def origin(c: CMD_ADD_HTLC): Origin = c.upstream match { case Left(id) => Local(id, Some(sender)) // we were the origin of the payment - case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) // this is a relayed payment + case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amount) // this is a relayed payment } def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: ByteVector32): Unit = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index e9104a7db4..e80ff51eca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -19,7 +19,8 @@ package fr.acinq.eclair.channel import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} +import fr.acinq.eclair.api.MilliSatoshiSerializer import fr.acinq.eclair.channel.Channel.ChannelError import fr.acinq.eclair.channel.Helpers.Closing.ClosingType import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} @@ -53,7 +54,7 @@ case class ChannelErrorOccured(channel: ActorRef, channelId: ByteVector32, remot case class NetworkFeePaid(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, tx: Transaction, fee: Satoshi, txType: String) extends ChannelEvent // NB: this event is only sent when the channel is available -case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, localBalanceMsat: Long, commitments: Commitments) extends ChannelEvent +case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, localBalance: MilliSatoshi, commitments: Commitments) extends ChannelEvent case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: Data) extends ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 3546f22af6..56fc890eb5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, Transaction} -import fr.acinq.eclair.UInt64 +import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} +import fr.acinq.eclair.{MilliSatoshi, UInt64} import fr.acinq.eclair.payment.Origin import fr.acinq.eclair.wire.{ChannelUpdate, UpdateAddHtlc} @@ -31,16 +31,16 @@ class ChannelException(val channelId: ByteVector32, message: String) extends Run // @formatter:off case class DebugTriggeredException (override val channelId: ByteVector32) extends ChannelException(channelId, "debug-mode triggered failure") case class InvalidChainHash (override val channelId: ByteVector32, local: ByteVector32, remote: ByteVector32) extends ChannelException(channelId, s"invalid chainHash (local=$local remote=$remote)") -case class InvalidFundingAmount (override val channelId: ByteVector32, fundingSatoshis: Long, min: Long, max: Long) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingSatoshis (min=$min max=$max)") -case class InvalidPushAmount (override val channelId: ByteVector32, pushMsat: Long, max: Long) extends ChannelException(channelId, s"invalid pushMsat=$pushMsat (max=$max)") +case class InvalidFundingAmount (override val channelId: ByteVector32, fundingAmount: Satoshi, min: Satoshi, max: Satoshi) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingAmount (min=$min max=$max)") +case class InvalidPushAmount (override val channelId: ByteVector32, pushAmount: MilliSatoshi, max: MilliSatoshi) extends ChannelException(channelId, s"invalid pushAmount=$pushAmount (max=$max)") case class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)") -case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimitSatoshis: Long, min: Long) extends ChannelException(channelId, s"dustLimitSatoshis=$dustLimitSatoshis is too small (min=$min)") -case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimitSatoshis: Long, max: Long) extends ChannelException(channelId, s"dustLimitSatoshis=$dustLimitSatoshis is too large (max=$max)") -case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimitSatoshis: Long, channelReserveSatoshis: Long) extends ChannelException(channelId, s"dustLimitSatoshis dustLimitSatoshis=$dustLimitSatoshis is above our channelReserveSatoshis=$channelReserveSatoshis") +case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimit: Satoshi, min: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too small (min=$min)") +case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimit: Satoshi, max: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too large (max=$max)") +case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimit: Satoshi, channelReserve: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is above our channelReserve=$channelReserve") case class ToSelfDelayTooHigh (override val channelId: ByteVector32, toSelfDelay: Int, max: Int) extends ChannelException(channelId, s"unreasonable to_self_delay=$toSelfDelay (max=$max)") -case class ChannelReserveTooHigh (override val channelId: ByteVector32, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio") -case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserveSatoshis: Long, dustLimitSatoshis: Long) extends ChannelException(channelId, s"their channelReserveSatoshis=$channelReserveSatoshis is below our dustLimitSatoshis=$dustLimitSatoshis") -case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocalMsat: Long, toRemoteMsat: Long, reserveSatoshis: Long) extends ChannelException(channelId, s"channel reserve is not met toLocalMsat=$toLocalMsat toRemoteMsat=$toRemoteMsat reserveSat=$reserveSatoshis") +case class ChannelReserveTooHigh (override val channelId: ByteVector32, channelReserve: Satoshi, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserve too high: reserve=$channelReserve fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio") +case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserve: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"their channelReserve=$channelReserve is below our dustLimit=$dustLimit") +case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocal: MilliSatoshi, toRemote: MilliSatoshi, reserve: Satoshi) extends ChannelException(channelId, s"channel reserve is not met toLocal=$toLocal toRemote=$toRemote reserve=$reserve") case class ChannelFundingError (override val channelId: ByteVector32) extends ChannelException(channelId, "channel funding error") case class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new htlcs, closing in progress") case class ClosingAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "closing already in progress") @@ -57,21 +57,21 @@ case class FeerateTooDifferent (override val channelId: ByteVect case class InvalidCommitmentSignature (override val channelId: ByteVector32, tx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: tx=$tx") case class InvalidHtlcSignature (override val channelId: ByteVector32, tx: Transaction) extends ChannelException(channelId, s"invalid htlc signature: tx=$tx") case class InvalidCloseSignature (override val channelId: ByteVector32, tx: Transaction) extends ChannelException(channelId, s"invalid close signature: tx=$tx") -case class InvalidCloseFee (override val channelId: ByteVector32, feeSatoshi: Long) extends ChannelException(channelId, s"invalid close fee: fee_satoshis=$feeSatoshi") +case class InvalidCloseFee (override val channelId: ByteVector32, fee: Satoshi) extends ChannelException(channelId, s"invalid close fee: fee_satoshis=$fee") case class HtlcSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual: $actual") case class ForcedLocalCommit (override val channelId: ByteVector32) extends ChannelException(channelId, s"forced local commit") case class UnexpectedHtlcId (override val channelId: ByteVector32, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual") case class ExpiryTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: minimum=$minimum actual=$actual blockCount=$blockCount") case class ExpiryTooBig (override val channelId: ByteVector32, maximum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too big: maximum=$maximum actual=$actual blockCount=$blockCount") -case class HtlcValueTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual") +case class HtlcValueTooSmall (override val channelId: ByteVector32, minimum: MilliSatoshi, actual: MilliSatoshi) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual") case class HtlcValueTooHighInFlight (override val channelId: ByteVector32, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual") case class TooManyAcceptedHtlcs (override val channelId: ByteVector32, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum") -case class InsufficientFunds (override val channelId: ByteVector32, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis") +case class InsufficientFunds (override val channelId: ByteVector32, amount: MilliSatoshi, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"insufficient funds: missing=$missing reserve=$reserve fees=$fees") case class InvalidHtlcPreimage (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id") case class UnknownHtlcId (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id") case class CannotExtractSharedSecret (override val channelId: ByteVector32, htlc: UpdateAddHtlc) extends ChannelException(channelId, s"can't extract shared secret: paymentHash=${htlc.paymentHash} onion=${htlc.onionRoutingPacket}") case class FundeeCannotSendUpdateFee (override val channelId: ByteVector32) extends ChannelException(channelId, s"only the funder should send update_fee messages") -case class CannotAffordFees (override val channelId: ByteVector32, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis") +case class CannotAffordFees (override val channelId: ByteVector32, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"can't pay the fee: missing=$missing reserve=$reserve fees=$fees") case class CannotSignWithoutChanges (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign when there are no changes") case class CannotSignBeforeRevocation (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign until next revocation hash is received") case class UnexpectedRevocation (override val channelId: ByteVector32) extends ChannelException(channelId, "received unexpected RevokeAndAck message") 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 48e6d6b57f..151cf582d5 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 @@ -21,10 +21,11 @@ import java.util.UUID import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.eclair.api.MilliSatoshiSerializer import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.{BitVector, ByteVector} @@ -75,7 +76,7 @@ case object ERR_INFORMATION_LEAK extends State 8888888888 Y8P 8888888888 888 Y888 888 "Y8888P" */ -case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte) +case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte) case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init) case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close case object INPUT_DISCONNECTED @@ -106,14 +107,14 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: OnionRoutingPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command +final case class CMD_ADD_HTLC(amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: Long, onion: OnionRoutingPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command final case class CMD_UPDATE_FEE(feeratePerKw: Long, commit: Boolean = false) extends Command final case object CMD_SIGN extends Command final case class CMD_CLOSE(scriptPubKey: Option[ByteVector]) extends Command -final case class CMD_UPDATE_RELAY_FEE(feeBaseMsat: Long, feeProportionalMillionths: Long) extends Command +final case class CMD_UPDATE_RELAY_FEE(feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends Command final case object CMD_FORCECLOSE extends Command final case object CMD_GETSTATE extends Command final case object CMD_GETSTATEDATA extends Command @@ -148,8 +149,8 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data -final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, lastSent: OpenChannel) extends Data -final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: Byte, lastSent: AcceptChannel) extends Data +final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, lastSent: OpenChannel) extends Data +final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: Byte, lastSent: AcceptChannel) extends Data final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, fundingTxFee: Satoshi, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], @@ -191,10 +192,10 @@ final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Com final case class LocalParams(nodeId: PublicKey, channelKeyPath: DeterministicWallet.KeyPath, - dustLimitSatoshis: Long, + dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, + channelReserve: Satoshi, + htlcMinimum: MilliSatoshi, toSelfDelay: Int, maxAcceptedHtlcs: Int, isFunder: Boolean, @@ -203,10 +204,10 @@ final case class LocalParams(nodeId: PublicKey, localFeatures: ByteVector) final case class RemoteParams(nodeId: PublicKey, - dustLimitSatoshis: Long, + dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, + channelReserve: Satoshi, + htlcMinimum: MilliSatoshi, toSelfDelay: Int, maxAcceptedHtlcs: Int, fundingPubKey: PublicKey, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 7c1bace272..21533f0dba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -19,13 +19,15 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} +import fr.acinq.eclair +import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, UInt64} +import fr.acinq.eclair.{Globals, MilliSatoshi, UInt64} // @formatter:off case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) { @@ -84,16 +86,16 @@ case class Commitments(channelVersion: ChannelVersion, val announceChannel: Boolean = (channelFlags & 0x01) != 0 - lazy val availableBalanceForSendMsat: Long = { + lazy val availableBalanceForSend: MilliSatoshi = { val reduced = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) - val feesMsat = if (localParams.isFunder) Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), reduced).amount * 1000 else 0 - math.max(reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - feesMsat, 0) + val feesMsat = if (localParams.isFunder) commitTxFee(remoteParams.dustLimit, reduced).toMilliSatoshi else MilliSatoshi(0) + maxOf(reduced.toRemote - remoteParams.channelReserve.toMilliSatoshi - feesMsat, MilliSatoshi(0)) } - lazy val availableBalanceForReceiveMsat: Long = { + lazy val availableBalanceForReceive: MilliSatoshi = { val reduced = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val feesMsat = if (localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(localParams.dustLimitSatoshis), reduced).amount * 1000 - math.max(reduced.toRemoteMsat - localParams.channelReserveSatoshis * 1000 - feesMsat, 0) + val feesMsat = if (localParams.isFunder) MilliSatoshi(0) else commitTxFee(localParams.dustLimit, reduced).toMilliSatoshi + maxOf(reduced.toRemote - localParams.channelReserve.toMilliSatoshi - feesMsat, MilliSatoshi(0)) } } @@ -132,12 +134,12 @@ object Commitments { return Left(ExpiryTooBig(commitments.channelId, maximum = maxExpiry, actual = cmd.cltvExpiry, blockCount = blockCount)) } - if (cmd.amountMsat < commitments.remoteParams.htlcMinimumMsat) { - return Left(HtlcValueTooSmall(commitments.channelId, minimum = commitments.remoteParams.htlcMinimumMsat, actual = cmd.amountMsat)) + if (cmd.amount < commitments.remoteParams.htlcMinimum) { + return Left(HtlcValueTooSmall(commitments.channelId, minimum = commitments.remoteParams.htlcMinimum, actual = cmd.amount)) } // let's compute the current commitment *as seen by them* with this change taken into account - val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) // we increment the local htlc index and add an entry to the origins map val commitments1 = addLocalProposal(commitments, add).copy(localNextHtlcId = commitments.localNextHtlcId + 1, originChannels = commitments.originChannels + (add.id -> origin)) // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation @@ -146,7 +148,7 @@ object Commitments { // the HTLC we are about to create is outgoing, but from their point of view it is incoming val outgoingHtlcs = reduced.htlcs.filter(_.direction == IN) - val htlcValueInFlight = UInt64(outgoingHtlcs.map(_.add.amountMsat).sum) + val htlcValueInFlight = UInt64(outgoingHtlcs.map(_.add.amountMsat).sum.toLong) if (htlcValueInFlight > commitments1.remoteParams.maxHtlcValueInFlightMsat) { // TODO: this should be a specific UPDATE error return Left(HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.remoteParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight)) @@ -158,10 +160,10 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is funder remote doesn't pay the fees - val fees = if (commitments1.localParams.isFunder) Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount else 0 - val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees - if (missing < 0) { - return Left(InsufficientFunds(commitments.channelId, amountMsat = cmd.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.remoteParams.channelReserveSatoshis, feesSatoshis = fees)) + val fees = if (commitments1.localParams.isFunder) commitTxFee(commitments1.remoteParams.dustLimit, reduced) else Satoshi(0) + val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees + if (missing < Satoshi(0)) { + return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missing, reserve = commitments1.remoteParams.channelReserve, fees = fees)) } Right(commitments1, add) @@ -172,8 +174,8 @@ object Commitments { throw UnexpectedHtlcId(commitments.channelId, expected = commitments.remoteNextHtlcId, actual = add.id) } - if (add.amountMsat < commitments.localParams.htlcMinimumMsat) { - throw HtlcValueTooSmall(commitments.channelId, minimum = commitments.localParams.htlcMinimumMsat, actual = add.amountMsat) + if (add.amountMsat < commitments.localParams.htlcMinimum) { + throw HtlcValueTooSmall(commitments.channelId, minimum = commitments.localParams.htlcMinimum, actual = add.amountMsat) } // let's compute the current commitment *as seen by us* including this change @@ -181,7 +183,7 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) val incomingHtlcs = reduced.htlcs.filter(_.direction == IN) - val htlcValueInFlight = UInt64(incomingHtlcs.map(_.add.amountMsat).sum) + val htlcValueInFlight = UInt64(incomingHtlcs.map(_.add.amountMsat).sum.toLong) if (htlcValueInFlight > commitments1.localParams.maxHtlcValueInFlightMsat) { throw HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.localParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight) } @@ -191,10 +193,10 @@ object Commitments { } // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - val fees = if (commitments1.localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(commitments1.localParams.dustLimitSatoshis), reduced).amount - val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees - if (missing < 0) { - throw InsufficientFunds(commitments.channelId, amountMsat = add.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) + val fees = if (commitments1.localParams.isFunder) Satoshi(0) else Transactions.commitTxFee(commitments1.localParams.dustLimit, reduced) + val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees + if (missing < Satoshi(0)) { + throw InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees) } commitments1 @@ -313,10 +315,10 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is funder remote doesn't pay the fees - val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount - val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees - if (missing < 0) { - throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) + val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) + val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees + if (missing < Satoshi(0)) { + throw CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees) } (commitments1, fee) @@ -347,10 +349,10 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount - val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees - if (missing < 0) { - throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) + val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) + val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees + if (missing < Satoshi(0)) { + throw CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees) } commitments1 @@ -527,8 +529,8 @@ object Commitments { val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } @@ -538,8 +540,8 @@ object Commitments { val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, !localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } @@ -574,18 +576,18 @@ object Commitments { def specs2String(commitments: Commitments): String = { s"""specs: |localcommit: - | toLocal: ${commitments.localCommit.spec.toLocalMsat} - | toRemote: ${commitments.localCommit.spec.toRemoteMsat} + | toLocal: ${commitments.localCommit.spec.toLocal} + | toRemote: ${commitments.localCommit.spec.toRemote} | htlcs: |${commitments.localCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")} |remotecommit: - | toLocal: ${commitments.remoteCommit.spec.toLocalMsat} - | toRemote: ${commitments.remoteCommit.spec.toRemoteMsat} + | toLocal: ${commitments.remoteCommit.spec.toLocal} + | toRemote: ${commitments.remoteCommit.spec.toRemote} | htlcs: |${commitments.remoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")} |next remotecommit: - | toLocal: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toLocalMsat).getOrElse("N/A")} - | toRemote: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toRemoteMsat).getOrElse("N/A")} + | toLocal: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toLocal).getOrElse("N/A")} + | toRemote: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toRemote).getOrElse("N/A")} | htlcs: |${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin } 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 5cbe075fcb..1f49e73429 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 @@ -20,6 +20,8 @@ import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.{OutPoint, _} +import fr.acinq.eclair +import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL @@ -88,10 +90,10 @@ object Helpers { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) throw InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash) - if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis >= Channel.MAX_FUNDING_SATOSHIS) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS) + if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis >= Channel.MAX_FUNDING) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING) // BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000. - if (open.pushMsat > 1000 * open.fundingSatoshis) throw InvalidPushAmount(open.temporaryChannelId, open.pushMsat, 1000 * open.fundingSatoshis) + if (open.pushMsat > open.fundingSatoshis.toMilliSatoshi) throw InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi) // BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large. if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.maxToLocalDelayBlocks) throw ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.maxToLocalDelayBlocks) @@ -107,8 +109,8 @@ object Helpers { // BOLT #2: The receiving node MUST fail the channel if both to_local and to_remote amounts for the initial commitment // transaction are less than or equal to channel_reserve_satoshis (see BOLT 3). - val (toLocalMsat, toRemoteMsat) = (open.pushMsat, open.fundingSatoshis * 1000 - open.pushMsat) - if (toLocalMsat < open.channelReserveSatoshis * 1000 && toRemoteMsat < open.channelReserveSatoshis * 1000) { + val (toLocalMsat, toRemoteMsat) = (open.pushMsat, open.fundingSatoshis.toMilliSatoshi - open.pushMsat) + if (toLocalMsat < open.channelReserveSatoshis.toMilliSatoshi && toRemoteMsat < open.channelReserveSatoshis.toMilliSatoshi) { throw ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis) } @@ -122,7 +124,7 @@ object Helpers { // we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment // now, but it will be done later when we receive `funding_created` - val reserveToFundingRatio = open.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1) + val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio) } @@ -151,7 +153,7 @@ object Helpers { // MUST reject the channel. Other fields have the same requirements as their counterparts in open_channel. if (open.channelReserveSatoshis < accept.dustLimitSatoshis) throw DustLimitAboveOurChannelReserve(accept.temporaryChannelId, accept.dustLimitSatoshis, open.channelReserveSatoshis) - val reserveToFundingRatio = accept.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1) + val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio) } @@ -219,10 +221,10 @@ object Helpers { case Left(waitingForRevocation) => waitingForRevocation.nextRemoteCommit case _ => commitments.remoteCommit } - val toRemoteSatoshis = remoteCommit.spec.toRemoteMsat / 1000 + val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi // NB: this is an approximation (we don't take network fees into account) - val result = toRemoteSatoshis > commitments.remoteParams.channelReserveSatoshis - log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserveSatoshis} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") + val result = toRemoteSatoshis > commitments.remoteParams.channelReserve + log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") result } @@ -252,24 +254,24 @@ object Helpers { * @param remoteFirstPerCommitmentPoint * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ - def makeFirstCommitTxs(keyManager: KeyManager, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { - val toLocalMsat = if (localParams.isFunder) fundingSatoshis * 1000 - pushMsat else pushMsat - val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingSatoshis * 1000 - pushMsat + def makeFirstCommitTxs(keyManager: KeyManager, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: Long, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { + val toLocalMsat = if (localParams.isFunder) fundingAmount.toMilliSatoshi - pushMsat else pushMsat + val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingAmount.toMilliSatoshi - pushMsat - val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toLocalMsat, toRemoteMsat = toRemoteMsat) - val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toRemoteMsat, toRemoteMsat = toLocalMsat) + val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) + val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocal = toRemoteMsat, toRemote = toLocalMsat) if (!localParams.isFunder) { // they are funder, therefore they pay the fee: we need to make sure they can afford it! - val toRemoteMsat = remoteSpec.toLocalMsat - val fees = Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), remoteSpec).amount - val missing = toRemoteMsat / 1000 - localParams.channelReserveSatoshis - fees - if (missing < 0) { - throw CannotAffordFees(temporaryChannelId, missingSatoshis = -1 * missing, reserveSatoshis = localParams.channelReserveSatoshis, feesSatoshis = fees) + val toRemoteMsat = remoteSpec.toLocal + val fees = commitTxFee(remoteParams.dustLimit, remoteSpec) + val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees + if (missing < Satoshi(0)) { + throw CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees) } } - val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) + val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0) val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) @@ -358,9 +360,9 @@ object Helpers { */ def nothingAtStake(data: HasCommitments): Boolean = data.commitments.localCommit.index == 0 && - data.commitments.localCommit.spec.toLocalMsat == 0 && + data.commitments.localCommit.spec.toLocal == MilliSatoshi(0) && data.commitments.remoteCommit.index == 0 && - data.commitments.remoteCommit.spec.toRemoteMsat == 0 && + data.commitments.remoteCommit.spec.toRemote == MilliSatoshi(0) && data.commitments.remoteNextCommitInfo.isRight /** @@ -453,10 +455,10 @@ object Helpers { require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey") log.debug(s"making closing tx with closingFee={} and commitments:\n{}", closingFee, Commitments.specs2String(commitments)) // TODO: check that - val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis)) + val dustLimitSatoshis = maxOf(localParams.dustLimit, remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath)) - val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig) + 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}}") (closingTx, closingSigned) @@ -467,7 +469,7 @@ object Helpers { val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount.amount).sum if (remoteClosingFee.amount > lastCommitFeeSatoshi) { log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.amount} lastCommitFeeSatoshi=$lastCommitFeeSatoshi") - throw InvalidCloseFee(commitments.channelId, remoteClosingFee.amount) + throw InvalidCloseFee(commitments.channelId, remoteClosingFee) } val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee) val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) @@ -508,7 +510,7 @@ object Helpers { // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output")(Try { - val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) + val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) Transactions.addSigs(claimDelayed, sig) }) @@ -539,7 +541,7 @@ object Helpers { generateTx("claim-htlc-delayed")(Try { val claimDelayed = Transactions.makeClaimDelayedOutputTx( txinfo.tx, - Satoshi(localParams.dustLimitSatoshis), + localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, @@ -590,7 +592,7 @@ object Helpers { // incoming htlc for which we have the preimage: we spend it directly case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get - val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) + val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputsAlreadyUsed, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig, preimage) @@ -600,7 +602,7 @@ object Helpers { // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { - val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) + val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputsAlreadyUsed, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig) @@ -630,7 +632,7 @@ object Helpers { val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) val mainTx = generateTx("claim-p2wpkh-output")(Try { - val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), + val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain) val sig = keyManager.sign(claimMain, keyManager.paymentPoint(commitments.localParams.channelKeyPath), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPubkey, sig) @@ -679,14 +681,14 @@ object Helpers { // first we will claim our main output right away val mainTx = generateTx("claim-p2wpkh-output")(Try { - val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) + val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) val sig = keyManager.sign(claimMain, keyManager.paymentPoint(localParams.channelKeyPath), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPubkey, sig) }) // then we punish them by stealing their main output val mainPenaltyTx = generateTx("main-penalty")(Try { - val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty) + val txinfo = Transactions.makeMainPenaltyTx(tx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty) val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) Transactions.addSigs(txinfo, sig) }) @@ -706,7 +708,7 @@ object Helpers { val htlcPenaltyTxs = tx.txOut.collect { case txOut if htlcsRedeemScripts.contains(txOut.publicKeyScript) => val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) generateTx("htlc-penalty")(Try { - val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, Satoshi(localParams.dustLimitSatoshis), localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) + val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) outputsAlreadyUsed = outputsAlreadyUsed + htlcPenalty.input.outPoint.index.toInt val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) @@ -762,7 +764,7 @@ object Helpers { val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) generateTx("claim-htlc-delayed-penalty")(Try { - val htlcDelayedPenalty = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) + val htlcDelayedPenalty = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) // we need to make sure that the tx is indeed valid diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala index 63dbe4d74f..5fccd441b1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} @@ -51,8 +51,8 @@ trait AuditDb { } -case class ChannelLifecycleEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacitySat: Long, isFunder: Boolean, isPrivate: Boolean, event: String) +case class ChannelLifecycleEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacity: Satoshi, isFunder: Boolean, isPrivate: Boolean, event: String) -case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, feeSat: Long, txType: String, timestamp: Long) +case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, fee: Satoshi, txType: String, timestamp: Long) -case class Stats(channelId: ByteVector32, avgPaymentAmountSatoshi: Long, paymentCount: Int, relayFeeSatoshi: Long, networkFeeSatoshi: Long) +case class Stats(channelId: ByteVector32, avgPaymentAmount: Satoshi, paymentCount: Int, relayFee: Satoshi, networkFee: Satoshi) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index b13e190446..b9f4aa838f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -17,7 +17,9 @@ package fr.acinq.eclair.db import java.util.UUID + import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.payment.PaymentRequest trait PaymentsDb { @@ -60,10 +62,10 @@ trait PaymentsDb { * Incoming payment object stored in DB. * * @param paymentHash identifier of the payment - * @param amountMsat amount of the payment, in milli-satoshis + * @param amount amount of the payment, in milli-satoshis * @param receivedAt absolute time in seconds since UNIX epoch when the payment was received. */ -case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, receivedAt: Long) +case class IncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long) /** * Sent payment is every payment that is sent by this node, they may not be finalized and @@ -72,12 +74,12 @@ case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, received * @param id internal payment identifier * @param paymentHash payment_hash * @param preimage the preimage of the payment_hash, known if the outgoing payment was successful - * @param amountMsat amount of the payment, in milli-satoshis + * @param amount amount of the payment, in milli-satoshis * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. * @param completedAt absolute time in seconds since UNIX epoch when the payment succeeded. * @param status current status of the payment. */ -case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, preimage:Option[ByteVector32], amountMsat: Long, createdAt: Long, completedAt: Option[Long], status: OutgoingPaymentStatus.Value) +case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, preimage:Option[ByteVector32], amount: MilliSatoshi, createdAt: Long, completedAt: Option[Long], status: OutgoingPaymentStatus.Value) object OutgoingPaymentStatus extends Enumeration { val PENDING = Value(1, "PENDING") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 519ed96b8d..c03bec2a84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -19,7 +19,8 @@ package fr.acinq.eclair.db.sqlite import java.sql.{Connection, Statement} import java.util.UUID import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.MilliSatoshi +import fr.acinq.bitcoin.Satoshi +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.channel.{AvailableBalanceChanged, Channel, ChannelErrorOccured, NetworkFeePaid} import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent, NetworkFee, Stats} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} @@ -83,9 +84,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { using(sqlite.prepareStatement("INSERT INTO balance_updated VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, e.channelId.toArray) statement.setBytes(2, e.commitments.remoteParams.nodeId.value.toArray) - statement.setLong(3, e.localBalanceMsat) + statement.setLong(3, e.localBalance.toLong) statement.setLong(4, e.commitments.commitInput.txOut.amount.toLong) - statement.setLong(5, e.commitments.remoteParams.channelReserveSatoshis) // remote decides what our reserve should be + statement.setLong(5, e.commitments.remoteParams.channelReserve.toLong) // remote decides what our reserve should be statement.setLong(6, Platform.currentTime) statement.executeUpdate() } @@ -94,7 +95,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { using(sqlite.prepareStatement("INSERT INTO channel_events VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, e.channelId.toArray) statement.setBytes(2, e.remoteNodeId.value.toArray) - statement.setLong(3, e.capacitySat) + statement.setLong(3, e.capacity.toLong) statement.setBoolean(4, e.isFunder) statement.setBoolean(5, e.isPrivate) statement.setString(6, e.event) @@ -225,7 +226,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { remoteNodeId = PublicKey(rs.getByteVector("node_id")), channelId = rs.getByteVector32("channel_id"), txId = rs.getByteVector32("tx_id"), - feeSat = rs.getLong("fee_sat"), + fee = Satoshi(rs.getLong("fee_sat")), txType = rs.getString("tx_type"), timestamp = rs.getLong("timestamp")) } @@ -267,10 +268,10 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { while (rs.next()) { q = q :+ Stats( channelId = rs.getByteVector32("channel_id"), - avgPaymentAmountSatoshi = rs.getLong("avg_payment_amount_sat"), + avgPaymentAmount = Satoshi(rs.getLong("avg_payment_amount_sat")), paymentCount = rs.getInt("payment_count"), - relayFeeSatoshi = rs.getLong("relay_fee_sat"), - networkFeeSatoshi = rs.getLong("network_fee_sat")) + relayFee = Satoshi(rs.getLong("relay_fee_sat")), + networkFee = Satoshi(rs.getLong("network_fee_sat"))) } q } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 8fd6ded566..127ec6be2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.payment.PaymentRequest import grizzled.slf4j.Logging import scala.collection.immutable.Queue import OutgoingPaymentStatus._ +import fr.acinq.eclair.MilliSatoshi import concurrent.duration._ import scala.compat.Platform @@ -47,7 +48,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, status) VALUES (?, ?, ?, ?, ?)")) { statement => statement.setString(1, sent.id.toString) statement.setBytes(2, sent.paymentHash.toArray) - statement.setLong(3, sent.amountMsat) + statement.setLong(3, sent.amount.toLong) statement.setLong(4, sent.createdAt) statement.setString(5, sent.status.toString) val res = statement.executeUpdate() @@ -76,7 +77,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), rs.getByteVector32Nullable("preimage"), - rs.getLong("amount_msat"), + MilliSatoshi(rs.getLong("amount_msat")), rs.getLong("created_at"), getNullableLong(rs, "completed_at"), OutgoingPaymentStatus.withName(rs.getString("status")) @@ -97,7 +98,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), rs.getByteVector32Nullable("preimage"), - rs.getLong("amount_msat"), + MilliSatoshi(rs.getLong("amount_msat")), rs.getLong("created_at"), getNullableLong(rs, "completed_at"), OutgoingPaymentStatus.withName(rs.getString("status")) @@ -116,7 +117,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), rs.getByteVector32Nullable("preimage"), - rs.getLong("amount_msat"), + MilliSatoshi(rs.getLong("amount_msat")), rs.getLong("created_at"), getNullableLong(rs, "completed_at"), OutgoingPaymentStatus.withName(rs.getString("status")) @@ -194,7 +195,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def addIncomingPayment(payment: IncomingPayment): Unit = { using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (?, ?) WHERE payment_hash = ?")) { statement => - statement.setLong(1, payment.amountMsat) + statement.setLong(1, payment.amount.toLong) statement.setLong(2, payment.receivedAt) statement.setBytes(3, payment.paymentHash.toArray) val res = statement.executeUpdate() @@ -207,7 +208,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))) + Some(IncomingPayment(rs.getByteVector32("payment_hash"), MilliSatoshi(rs.getLong("received_msat")), rs.getLong("received_at"))) } else { None } @@ -219,7 +220,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") var q: Queue[IncomingPayment] = Queue() while (rs.next()) { - q = q :+ IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")) + q = q :+ IncomingPayment(rs.getByteVector32("payment_hash"), MilliSatoshi(rs.getLong("received_msat")), rs.getLong("received_at")) } q } 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 64f77ea547..32c4d52cf2 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 @@ -25,7 +25,8 @@ 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, DeterministicWallet, MilliSatoshi, Protocol, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, Protocol, Satoshi} +import fr.acinq.eclair import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler @@ -282,20 +283,20 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A stay case Event(c: Peer.OpenChannel, d: ConnectedData) => - val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis.toLong, origin_opt = Some(sender)) + val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis, origin_opt = Some(sender)) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32 val channelFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(msg: wire.OpenChannel, d: ConnectedData) => d.transport ! TransportHandler.ReadAck(msg) d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => - val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingSatoshis = msg.fundingSatoshis, origin_opt = None) + val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) val temporaryChannelId = msg.temporaryChannelId log.info(s"accepting a new channel to $remoteNodeId temporaryChannelId=$temporaryChannelId localParams=$localParams") channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.transport, d.remoteInit) @@ -525,9 +526,9 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A case DISCONNECTED -> _ if nodeParams.autoReconnect => cancelTimer(RECONNECT_TIMER) } - def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingSatoshis: Long, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { + def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash) - val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingSatoshis) + val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingAmount) val channel = spawnChannel(nodeParams, origin_opt) (channel, localParams) } @@ -607,7 +608,7 @@ object Peer { case class Disconnect(nodeId: PublicKey) case object ResumeAnnouncements case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, fundingTxFeeratePerKw_opt: Option[Long], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) { - require(fundingSatoshis.amount < Channel.MAX_FUNDING_SATOSHIS, s"fundingSatoshis must be less than ${Channel.MAX_FUNDING_SATOSHIS}") + require(fundingSatoshis < Channel.MAX_FUNDING, s"fundingSatoshis must be less than ${Channel.MAX_FUNDING}") require(pushMsat.amount <= 1000 * fundingSatoshis.amount, s"pushMsat must be less or equal to fundingSatoshis") require(fundingSatoshis.amount >= 0, s"fundingSatoshis must be positive") require(pushMsat.amount >= 0, s"pushMsat must be positive") @@ -630,22 +631,22 @@ object Peer { // @formatter:on - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { val entropy = new Array[Byte](16) secureRandom.nextBytes(entropy) val bis = new ByteArrayInputStream(entropy) val channelKeyPath = DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN))) - makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingSatoshis, channelKeyPath) + makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingAmount, channelKeyPath) } - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long, channelKeyPath: DeterministicWallet.KeyPath): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi, channelKeyPath: DeterministicWallet.KeyPath): LocalParams = { LocalParams( nodeParams.nodeId, channelKeyPath, - dustLimitSatoshis = nodeParams.dustLimitSatoshis, + dustLimit = nodeParams.dustLimit, maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = Math.max((nodeParams.reserveToFundingRatio * fundingSatoshis).toLong, nodeParams.dustLimitSatoshis), // BOLT #2: make sure that our reserve is above our dust limit - htlcMinimumMsat = nodeParams.htlcMinimumMsat, + channelReserve = maxOf(Satoshi((nodeParams.reserveToFundingRatio * fundingAmount.toLong).toLong), nodeParams.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit + htlcMinimum = nodeParams.htlcMinimum, toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, defaultFinalScriptPubKey = defaultFinalScriptPubKey, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index 44adabf9eb..3ae5f2ffed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -126,7 +126,7 @@ package object eclair { * @param msat amount in millisatoshi * @return the fee (in msat) that a node should be paid to forward an HTLC of 'amount' millisatoshis */ - def nodeFee(baseMsat: Long, proportional: Long, msat: Long): Long = baseMsat + (proportional * msat) / 1000000 + def nodeFee(baseMsat: MilliSatoshi, proportional: Long, msat: MilliSatoshi): MilliSatoshi = baseMsat + (msat * proportional) / 1000000 /** * @@ -159,4 +159,54 @@ package object eclair { * We use this in the context of timestamp filtering, when we don't need an upper bound. */ val MaxEpochSeconds = Duration.fromNanos(Long.MaxValue).toSeconds + + /** + * One MilliSatoshi is a thousand of a Satoshi, the smallest unit usable in bitcoin + * @param amount + */ + case class MilliSatoshi(amount: Long) { + // @formatter:off + def toLong = amount + def +(other: MilliSatoshi) = MilliSatoshi(amount + other.amount) + def -(other: MilliSatoshi) = MilliSatoshi(amount - other.amount) + def *(m: Long) = MilliSatoshi(amount * m) + def /(d: Long) = MilliSatoshi(amount / d) + def compare(other: MilliSatoshi): Int = if (amount == other.amount) 0 else if (amount < other.amount) -1 else 1 + def <= (that: MilliSatoshi): Boolean = compare(that) <= 0 + def >= (that: MilliSatoshi): Boolean = compare(that) >= 0 + def < (that: MilliSatoshi): Boolean = compare(that) < 0 + def > (that: MilliSatoshi): Boolean = compare(that) > 0 + def unary_-() = MilliSatoshi(-amount) + def truncateToSatoshi: Satoshi = Satoshi(amount / 1000) + // @formatter:on + } + + implicit class ToMilliSatoshiConversion(amount: BtcAmount) { + def toMilliSatoshi: MilliSatoshi = amount match { + case sat: Satoshi => satoshi2millisatoshi(sat) + case millis: MilliBtc => satoshi2millisatoshi(millibtc2satoshi(millis)) + case bitcoin: Btc => satoshi2millisatoshi(btc2satoshi(bitcoin)) + } + } + + implicit object NumericMilliSatoshi extends Numeric[MilliSatoshi] { + override def plus(x: MilliSatoshi, y: MilliSatoshi): MilliSatoshi = MilliSatoshi(x.amount + y.amount) + override def minus(x: MilliSatoshi, y: MilliSatoshi): MilliSatoshi = MilliSatoshi(x.amount - y.amount) + override def times(x: MilliSatoshi, y: MilliSatoshi): MilliSatoshi = MilliSatoshi(x.amount * y.amount) + override def negate(x: MilliSatoshi): MilliSatoshi = MilliSatoshi(-x.amount) + override def fromInt(x: Int): MilliSatoshi = MilliSatoshi(x) + override def toInt(x: MilliSatoshi): Int = x.toLong.toInt + override def toLong(x: MilliSatoshi): Long = x.toLong + override def toFloat(x: MilliSatoshi): Float = x.toLong + override def toDouble(x: MilliSatoshi): Double = x.toLong + override def compare(x: MilliSatoshi, y: MilliSatoshi): Int = x.compare(y) + } + + private def satoshi2millisatoshi(input: Satoshi): MilliSatoshi = MilliSatoshi(input.amount * 1000L) + + def maxOf(x: MilliSatoshi, y: MilliSatoshi) = MilliSatoshi(Math.max(x.amount, y.amount)) + def minOf(x: MilliSatoshi, y: MilliSatoshi) = MilliSatoshi(Math.min(x.amount, y.amount)) + def maxOf(x: Satoshi, y: Satoshi) = Satoshi(Math.max(x.amount, y.amount)) + def minOf(x: Satoshi, y: Satoshi) = Satoshi(Math.min(x.amount, y.amount)) + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala index 6136b928e4..0c79bd663c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala @@ -18,12 +18,10 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, Props} import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.NodeParams -import fr.acinq.eclair.channel.Channel.{LocalError, RemoteError} +import fr.acinq.eclair.{MilliSatoshi, NodeParams} import fr.acinq.eclair.channel.Helpers.Closing.{LocalClose, MutualClose, RecoveryClose, RemoteClose, RevokedClose} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent} - import scala.concurrent.ExecutionContext import scala.concurrent.duration._ @@ -57,7 +55,7 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging { case e: ChannelStateChanged => e match { case ChannelStateChanged(_, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED, NORMAL, d: DATA_NORMAL) => - db.add(ChannelLifecycleEvent(d.channelId, remoteNodeId, d.commitments.commitInput.txOut.amount.toLong, d.commitments.localParams.isFunder, !d.commitments.announceChannel, "created")) + db.add(ChannelLifecycleEvent(d.channelId, remoteNodeId, d.commitments.commitInput.txOut.amount, d.commitments.localParams.isFunder, !d.commitments.announceChannel, "created")) case _ => () } @@ -69,7 +67,7 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging { case RecoveryClose => "recovery" case RevokedClose => "revoked" } - db.add(ChannelLifecycleEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount.toLong, e.commitments.localParams.isFunder, !e.commitments.announceChannel, event)) + db.add(ChannelLifecycleEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount, e.commitments.localParams.isFunder, !e.commitments.announceChannel, event)) } @@ -109,11 +107,11 @@ class BalanceEventThrottler(db: AuditDb) extends Actor with ActorLogging { case ProcessEvent(channelId) => pending.get(channelId) match { case Some(BalanceUpdate(first, last)) => - if (first.commitments.remoteCommit.spec.toRemoteMsat == last.localBalanceMsat) { + if (first.commitments.remoteCommit.spec.toRemote == last.localBalance) { // we don't log anything if the balance didn't change (e.g. it was a probe payment) log.info(s"ignoring balance event for channelId=$channelId (changed was discarded)") } else { - log.info(s"processing balance event for channelId=$channelId balance=${first.localBalanceMsat}->${last.localBalanceMsat}") + log.info(s"processing balance event for channelId=$channelId balance=${first.localBalance}->${last.localBalance}") // we log the last event, which contains the most up to date balance db.add(last) context.become(run(pending - channelId)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index 94e33deb94..71396e096e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -21,8 +21,8 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure, SendPayment} import fr.acinq.eclair.router.{Announcements, Data} -import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails} -import fr.acinq.eclair.{NodeParams, randomBytes32, secureRandom} +import fr.acinq.eclair.wire.IncorrectOrUnknownPaymentDetails +import fr.acinq.eclair.{MilliSatoshi, NodeParams, randomBytes32, secureRandom} import scala.concurrent.duration._ @@ -83,7 +83,7 @@ object Autoprobe { val PROBING_INTERVAL = 20 seconds - val PAYMENT_AMOUNT_MSAT = 100 * 1000 // this is below dust_limit so there won't be an output in the commitment tx + val PAYMENT_AMOUNT_MSAT = MilliSatoshi(100 * 1000) // this is below dust_limit so there won't be an output in the commitment tx object TickProbe diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 1070f3ef37..d936a1474a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, Props, Status} -import fr.acinq.bitcoin.{Crypto, MilliSatoshi} +import fr.acinq.bitcoin.{Crypto} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Channel} import fr.acinq.eclair.db.IncomingPayment import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment @@ -69,18 +69,18 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) case _ if htlc.cltvExpiry < minFinalExpiry => sender ! CMD_FAIL_HTLC(htlc.id, Right(FinalExpiryTooSoon), commit = true) - case Some(amount) if MilliSatoshi(htlc.amountMsat) < amount => - log.warning(s"received payment with amount too small for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") + case Some(amount) if htlc.amountMsat < amount => + log.warning(s"received payment with amount too small for paymentHash=${htlc.paymentHash} amount=${htlc.amountMsat}") sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) - case Some(amount) if MilliSatoshi(htlc.amountMsat) > amount * 2 => - log.warning(s"received payment with amount too large for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") + case Some(amount) if htlc.amountMsat > amount * 2 => + log.warning(s"received payment with amount too large for paymentHash=${htlc.paymentHash} amount=${htlc.amountMsat}") sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) case _ => - log.info(s"received payment for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") + log.info(s"received payment for paymentHash=${htlc.paymentHash} amount=${htlc.amountMsat}") // amount is correct or was not specified in the payment request nodeParams.db.payments.addIncomingPayment(IncomingPayment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime)) sender ! CMD_FULFILL_HTLC(htlc.id, paymentPreimage, commit = true) - context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash, htlc.channelId)) + context.system.eventStream.publish(PaymentReceived(htlc.amountMsat, htlc.paymentHash, htlc.channelId)) } case None => sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 5901dce810..7b0f1b55c2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -17,7 +17,10 @@ package fr.acinq.eclair.payment import java.util.UUID -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.MilliSatoshi + import scala.compat.Platform /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 907a68a25c..74374ec9d5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -20,7 +20,7 @@ import java.util.UUID import akka.actor.{ActorRef, FSM, Props, Status} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} @@ -47,14 +47,14 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis when(WAITING_FOR_REQUEST) { case Event(c: SendPaymentToRoute, WaitingForRequest) => - val send = SendPayment(c.amountMsat, c.paymentHash, c.hops.last, finalCltvExpiry = c.finalCltvExpiry, maxAttempts = 1) - paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amountMsat, Platform.currentTime, None, OutgoingPaymentStatus.PENDING)) + val send = SendPayment(c.amount, c.paymentHash, c.hops.last, finalCltvExpiry = c.finalCltvExpiry, maxAttempts = 1) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amount, Platform.currentTime, None, OutgoingPaymentStatus.PENDING)) router ! FinalizeRoute(c.hops) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, failures = Nil) case Event(c: SendPayment, WaitingForRequest) => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) - paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amountMsat, Platform.currentTime, None, OutgoingPaymentStatus.PENDING)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.assistedRoutes, routeParams = c.routeParams) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amount, Platform.currentTime, None, OutgoingPaymentStatus.PENDING)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } @@ -65,7 +65,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis // we add one block in order to not have our htlc fail when a new block has just been found val finalExpiry = Globals.blockCount.get().toInt + c.finalCltvExpiry.toInt + 1 - val (cmd, sharedSecrets) = buildCommand(id, c.amountMsat, finalExpiry, c.paymentHash, hops) + val (cmd, sharedSecrets) = buildCommand(id,c.amount, finalExpiry, c.paymentHash, hops) register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops) @@ -80,8 +80,8 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED, preimage = Some(fulfill.paymentPreimage)) - reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) - context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) + reply(s, PaymentSucceeded(id, cmd.amount, c.paymentHash, fulfill.paymentPreimage, hops)) + context.system.eventStream.publish(PaymentSent(id, c.amount, cmd.amount - c.amount, cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => @@ -111,12 +111,12 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis // in that case we don't know which node is sending garbage, let's try to blacklist all nodes except the one we are directly connected to and the destination node val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1) log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops)) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") // let's try to route around this node - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") @@ -144,18 +144,18 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis // in any case, we forward the update to the router router ! failureMessage.update // let's try again, router will have updated its state - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.assistedRoutes, ignoreNodes, ignoreChannels, c.routeParams) } else { // this node is fishy, it gave us a bad sig!! let's filter it out log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}") - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") // let's try again without the channel outgoing from nodeId val faultyChannel = hops.find(_.nodeId == nodeId).map(hop => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) } @@ -175,7 +175,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis } else { log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})") val faultyChannel = ChannelDesc(hops.head.lastUpdate.shortChannelId, hops.head.nodeId, hops.head.nextNodeId) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ LocalFailure(t)) } @@ -198,21 +198,21 @@ object PaymentLifecycle { def props(nodeParams: NodeParams, id: UUID, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], nodeParams, id, router, register) // @formatter:off - case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None, paymentPreimage: Option[ByteVector32] = None) + 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) sealed trait GenericSendPayment - case class SendPaymentToRoute(amountMsat: Long, paymentHash: ByteVector32, hops: Seq[PublicKey], finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY) extends GenericSendPayment - case class SendPayment(amountMsat: Long, + case class SendPaymentToRoute(amount: MilliSatoshi, paymentHash: ByteVector32, hops: Seq[PublicKey], finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY) extends GenericSendPayment + case class SendPayment(amount: MilliSatoshi, paymentHash: ByteVector32, targetNodeId: PublicKey, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY, maxAttempts: Int, routeParams: Option[RouteParams] = None) extends GenericSendPayment { - require(amountMsat > 0, s"amountMsat must be > 0") + require(amount > MilliSatoshi(0), s"amountMsat must be > 0") } sealed trait PaymentResult - case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees + case class PaymentSucceeded(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees sealed trait PaymentFailure case class LocalFailure(t: Throwable) extends PaymentFailure case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure @@ -246,7 +246,7 @@ object PaymentLifecycle { /** * - * @param finalAmountMsat the final htlc amount in millisatoshis + * @param finalAmount the final htlc amount in millisatoshis * @param finalExpiry the final htlc expiry in number of blocks * @param hops the hops as computed by the router + extra routes from payment request * @return a (firstAmountMsat, firstExpiry, payloads) tuple where: @@ -254,19 +254,19 @@ object PaymentLifecycle { * - firstExpiry is the cltv expiry for the first htlc in the route * - a sequence of payloads that will be used to build the onion */ - def buildPayloads(finalAmountMsat: Long, finalExpiry: Long, hops: Seq[Hop]): (Long, Long, Seq[PerHopPayload]) = - hops.reverse.foldLeft((finalAmountMsat, finalExpiry, PerHopPayload(ShortChannelId(0L), finalAmountMsat, finalExpiry) :: Nil)) { + def buildPayloads(finalAmount: MilliSatoshi, finalExpiry: Long, hops: Seq[Hop]): (MilliSatoshi, Long, Seq[PerHopPayload]) = + hops.reverse.foldLeft((finalAmount, finalExpiry, PerHopPayload(ShortChannelId(0L), finalAmount, finalExpiry) :: Nil)) { case ((msat, expiry, payloads), hop) => val nextFee = nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, msat) (msat + nextFee, expiry + hop.lastUpdate.cltvExpiryDelta, PerHopPayload(hop.lastUpdate.shortChannelId, msat, expiry) +: payloads) } - def buildCommand(id: UUID, finalAmountMsat: Long, finalExpiry: Long, paymentHash: ByteVector32, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = { - val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) + def buildCommand(id: UUID, finalAmount: MilliSatoshi, finalExpiry: Long, paymentHash: ByteVector32, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = { + val (firstAmount, firstExpiry, payloads) = buildPayloads(finalAmount, finalExpiry, hops.drop(1)) val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion.packet, upstream = Left(id), commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmount, paymentHash, firstExpiry, onion.packet, upstream = Left(id), commit = true) -> onion.sharedSecrets } /** 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 b5c6da04f9..fc6ea0ca3e 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 @@ -16,13 +16,14 @@ package fr.acinq.eclair.payment +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{MilliSatoshi, _} -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.payment.PaymentRequest._ import scodec.Codec import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} + import scala.concurrent.duration._ import scala.compat.Platform import scala.util.Try diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index d5d6b9f17e..7112bdd0ba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -21,14 +21,14 @@ import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentSucceeded} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{NodeParams, ShortChannelId, nodeFee} +import fr.acinq.eclair.{MilliSatoshi, NodeParams, ShortChannelId, nodeFee} import grizzled.slf4j.Logging import scodec.{Attempt, DecodeResult} @@ -38,7 +38,7 @@ import scala.collection.mutable sealed trait Origin case class Local(id: UUID, sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors -case class Relayed(originChannelId: ByteVector32, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin +case class Relayed(originChannelId: ByteVector32, originHtlcId: Long, amountIn: MilliSatoshi, amountOut: MilliSatoshi) extends Origin sealed trait ForwardMessage case class ForwardAdd(add: UpdateAddHtlc, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends ForwardMessage @@ -47,7 +47,7 @@ case class ForwardFail(fail: UpdateFailHtlc, to: Origin, htlc: UpdateAddHtlc) ex case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin, htlc: UpdateAddHtlc) extends ForwardMessage case object GetUsableBalances -case class UsableBalances(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSendMsat: Long, canReceiveMsat: Long, isPublic: Boolean) +case class UsableBalances(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean) // @formatter:on @@ -77,8 +77,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR .map(o => UsableBalances( remoteNodeId = o.nextNodeId, shortChannelId = o.channelUpdate.shortChannelId, - canSendMsat = o.commitments.availableBalanceForSendMsat, - canReceiveMsat = o.commitments.availableBalanceForReceiveMsat, + canSend = o.commitments.availableBalanceForSend, + canReceive = o.commitments.availableBalanceForReceive, isPublic = o.commitments.announceChannel)) case LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments) => @@ -158,17 +158,17 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR to match { case Local(id, None) => val feesPaid = MilliSatoshi(0) - context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) + context.system.eventStream.publish(PaymentSent(id, add.amountMsat, feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) // we sent the payment, but we probably restarted and the reference to the original sender was lost, // we publish the failure on the event stream and update the status in paymentDb nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED, Some(fulfill.paymentPreimage)) context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // case Local(_, Some(sender)) => sender ! fulfill - case Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut) => + case Relayed(originChannelId, originHtlcId, amountIn, amountOut) => val cmd = CMD_FULFILL_HTLC(originHtlcId, fulfill.paymentPreimage, commit = true) commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd) - context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId)) + context.system.eventStream.publish(PaymentRelayed(amountIn, amountOut, add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId)) } case ForwardFail(fail, to, add) => @@ -215,7 +215,7 @@ object Relayer extends Logging { sealed trait NextPayload case class FinalPayload(add: UpdateAddHtlc, payload: PerHopPayload) extends NextPayload case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: OnionRoutingPacket) extends NextPayload { - val relayFeeMsat: Long = add.amountMsat - payload.amtToForward + val relayFeeMsat: MilliSatoshi = add.amountMsat - payload.amtToForward val expiryDelta: Long = add.cltvExpiry - payload.outgoingCltvValue } // @formatter:on @@ -330,10 +330,10 @@ object Relayer extends Logging { val channelInfo_opt = channelUpdates.get(shortChannelId) val channelUpdate_opt = channelInfo_opt.map(_.channelUpdate) val relayResult = relayOrFail(relayPayload, channelUpdate_opt) - log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.commitments.availableBalanceForSendMsat).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult) + log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.commitments.availableBalanceForSend).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult) (shortChannelId, channelInfo_opt, relayResult) } - .collect { case (shortChannelId, Some(channelInfo), _: RelaySuccess) => (shortChannelId, channelInfo.commitments.availableBalanceForSendMsat) } + .collect { case (shortChannelId, Some(channelInfo), _: RelaySuccess) => (shortChannelId, channelInfo.commitments.availableBalanceForSend) } .filter(_._2 > relayPayload.payload.amtToForward) // we only keep channels that have enough balance to handle this payment .toList // needed for ordering .sortBy(_._2) // we want to use the channel with the lowest available balance that can process the payment diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 1e76f48b7b..abbfd61d4e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Features, ShortChannelId, serializationResult} +import fr.acinq.eclair.{Features, MilliSatoshi, ShortChannelId, serializationResult} import scodec.bits.{BitVector, ByteVector} import shapeless.HNil @@ -37,7 +37,7 @@ object Announcements { def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: ByteVector, addresses: List[NodeAddress], unknownFields: ByteVector): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: unknownFields :: HNil)))) - def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: Long, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Option[Long], unknownFields: ByteVector): ByteVector = + def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: Long, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: Int, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], unknownFields: ByteVector): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: unknownFields :: HNil)))) def signChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { @@ -128,7 +128,7 @@ object Announcements { def makeChannelFlags(isNode1: Boolean, enable: Boolean): Byte = BitVector.bits(!enable :: !isNode1 :: Nil).padLeft(8).toByte() - def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Long, enable: Boolean = true, timestamp: Long = Platform.currentTime.milliseconds.toSeconds): ChannelUpdate = { + def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: Long = Platform.currentTime.milliseconds.toSeconds): ChannelUpdate = { val messageFlags = makeMessageFlags(hasOptionChannelHtlcMax = true) // NB: we always support option_channel_htlc_max val channelFlags = makeChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), enable = enable) val htlcMaximumMsatOpt = Some(htlcMaximumMsat) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 178036a3ae..27e92a3238 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -29,7 +29,7 @@ object Graph { // @formatter:off // A compound weight for an edge, weight is obtained with (cost X factor),'cost' contains the actual amount+fees in millisatoshi, 'cltvCumulative' the total CLTV necessary to reach this edge - case class RichWeight(cost: Long, length: Int, cltv: Int, weight: Double) extends Ordered[RichWeight] { + case class RichWeight(cost: MilliSatoshi, length: Int, cltv: Int, weight: Double) extends Ordered[RichWeight] { override def compare(that: RichWeight): Int = this.weight.compareTo(that.weight) } case class WeightRatios(cltvDeltaFactor: Double, ageFactor: Double, capacityFactor: Double) { @@ -62,7 +62,7 @@ object Graph { * @param graph * @param sourceNode * @param targetNode - * @param amountMsat + * @param amount * @param pathsToFind * @param wr an object containing the ratios used to 'weight' edges when searching for the shortest path * @param currentBlockHeight the height of the chain tip (latest block) @@ -72,7 +72,7 @@ object Graph { def yenKshortestPaths(graph: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, - amountMsat: Long, + amount: MilliSatoshi, ignoredEdges: Set[ChannelDesc], extraEdges: Set[GraphEdge], pathsToFind: Int, @@ -88,9 +88,9 @@ object Graph { val candidates = new mutable.PriorityQueue[WeightedPath] // find the shortest path, k = 0 - val initialWeight = RichWeight(cost = amountMsat, 0, 0, 0) + val initialWeight = RichWeight(cost = amount, 0, 0, 0) val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, extraEdges, initialWeight, boundaries, currentBlockHeight, wr) - shortestPaths += WeightedPath(shortestPath, pathWeight(shortestPath, amountMsat, isPartial = false, currentBlockHeight, wr)) + shortestPaths += WeightedPath(shortestPath, pathWeight(shortestPath, amount, isPartial = false, currentBlockHeight, wr)) // avoid returning a list with an empty path if (shortestPath.isEmpty) return Seq.empty @@ -110,7 +110,7 @@ object Graph { // select the sub-path from the source to the spur node of the k-th previous shortest path val rootPathEdges = if (i == 0) prevShortestPath.head :: Nil else prevShortestPath.take(i) - val rootPathWeight = pathWeight(rootPathEdges, amountMsat, isPartial = true, currentBlockHeight, wr) + val rootPathWeight = pathWeight(rootPathEdges, amount, isPartial = true, currentBlockHeight, wr) // links to be removed that are part of the previous shortest path and which share the same root path val edgesToIgnore = shortestPaths.flatMap { weightedPath => @@ -136,7 +136,7 @@ object Graph { case false => rootPathEdges ++ spurPath } - val candidatePath = WeightedPath(totalPath, pathWeight(totalPath, amountMsat, isPartial = false, currentBlockHeight, wr)) + val candidatePath = WeightedPath(totalPath, pathWeight(totalPath, amount, isPartial = false, currentBlockHeight, wr)) if (boundaries(candidatePath.weight) && !shortestPaths.contains(candidatePath) && !candidates.exists(_ == candidatePath)) { candidates.enqueue(candidatePath) @@ -237,7 +237,7 @@ object Graph { // we call containsKey first because "getOrDefault" is not available in JDK7 val neighborCost = weight.containsKey(neighbor) match { - case false => RichWeight(Long.MaxValue, Int.MaxValue, Int.MaxValue, Double.MaxValue) + case false => RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, Int.MaxValue, Double.MaxValue) case true => weight.get(neighbor) } @@ -281,7 +281,7 @@ object Graph { private def edgeWeight(edge: GraphEdge, prev: RichWeight, isNeighborTarget: Boolean, currentBlockHeight: Long, weightRatios: Option[WeightRatios]): RichWeight = weightRatios match { case None => val edgeCost = if (isNeighborTarget) prev.cost else edgeFeeCost(edge, prev.cost) - RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + edge.update.cltvExpiryDelta, weight = edgeCost) + RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + edge.update.cltvExpiryDelta, weight = edgeCost.toLong) case Some(wr) => import RoutingHeuristics._ @@ -291,8 +291,8 @@ object Graph { val ageFactor = normalize(channelBlockHeight, min = currentBlockHeight - BLOCK_TIME_TWO_MONTHS, max = currentBlockHeight) // Every edge is weighted by channel capacity, larger channels add less weight - val edgeMaxCapacity = edge.update.htlcMaximumMsat.getOrElse(CAPACITY_CHANNEL_LOW_MSAT) - val capFactor = 1 - normalize(edgeMaxCapacity, CAPACITY_CHANNEL_LOW_MSAT, CAPACITY_CHANNEL_HIGH_MSAT) + val edgeMaxCapacity = edge.update.htlcMaximumMsat.getOrElse(CAPACITY_CHANNEL_LOW) + val capFactor = 1 - normalize(edgeMaxCapacity.toLong, CAPACITY_CHANNEL_LOW.toLong, CAPACITY_CHANNEL_HIGH.toLong) // Every edge is weighted by its clvt-delta value, normalized val channelCltvDelta = edge.update.cltvExpiryDelta @@ -303,7 +303,7 @@ object Graph { // NB we're guaranteed to have weightRatios and factors > 0 val factor = (cltvFactor * wr.cltvDeltaFactor) + (ageFactor * wr.ageFactor) + (capFactor * wr.capacityFactor) - val edgeWeight = if (isNeighborTarget) prev.weight else prev.weight + edgeCost * factor + val edgeWeight = if (isNeighborTarget) prev.weight else prev.weight + edgeCost.toLong * factor RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + channelCltvDelta, weight = edgeWeight) } @@ -317,17 +317,17 @@ object Graph { * @param amountWithFees the value that this edge will have to carry along * @return the new amount updated with the necessary fees for this edge */ - private def edgeFeeCost(edge: GraphEdge, amountWithFees: Long): Long = { - if(edgeHasZeroFee(edge)) amountWithFees + nodeFee(baseMsat = 1, proportional = 0, amountWithFees) + private def edgeFeeCost(edge: GraphEdge, amountWithFees: MilliSatoshi):MilliSatoshi = { + if(edgeHasZeroFee(edge)) amountWithFees + nodeFee(baseMsat = MilliSatoshi(1), proportional = 0, amountWithFees) else amountWithFees + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountWithFees) } private def edgeHasZeroFee(edge: GraphEdge): Boolean = { - edge.update.feeBaseMsat == 0 && edge.update.feeProportionalMillionths == 0 + edge.update.feeBaseMsat.toLong == 0 && edge.update.feeProportionalMillionths == 0 } // Calculates the total cost of a path (amount + fees), direct channels with the source will have a cost of 0 (pay no fees) - def pathWeight(path: Seq[GraphEdge], amountMsat: Long, isPartial: Boolean, currentBlockHeight: Long, wr: Option[WeightRatios]): RichWeight = { + def pathWeight(path: Seq[GraphEdge], amountMsat: MilliSatoshi, isPartial: Boolean, currentBlockHeight: Long, wr: Option[WeightRatios]): RichWeight = { path.drop(if (isPartial) 0 else 1).foldRight(RichWeight(amountMsat, 0, 0, 0)) { (edge, prev) => edgeWeight(edge, prev, false, currentBlockHeight, wr) } @@ -340,8 +340,8 @@ object Graph { val BLOCK_TIME_TWO_MONTHS = 8640 // Low/High bound for channel capacity - val CAPACITY_CHANNEL_LOW_MSAT = 1000 * 1000L // 1000 sat - val CAPACITY_CHANNEL_HIGH_MSAT = Channel.MAX_FUNDING_SATOSHIS * 1000L + val CAPACITY_CHANNEL_LOW = MilliSatoshi(1000 * 1000L) // 1000 sat + val CAPACITY_CHANNEL_HIGH = Channel.MAX_FUNDING.toMilliSatoshi // Low/High bound for CLTV channel value val CLTV_LOW = 9 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 af29804f03..2897f0778b 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 @@ -21,7 +21,7 @@ import java.util.zip.Adler32 import akka.Done import akka.actor.{ActorRef, Props, Status} import akka.event.Logging.MDC -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} import fr.acinq.eclair._ @@ -50,7 +50,7 @@ import scala.util.{Random, Try} case class RouterConf(randomizeRouteSelection: Boolean, channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, - searchMaxFeeBaseSat: Long, + searchMaxFeeBase: Satoshi, searchMaxFeePct: Double, searchMaxRouteLength: Int, searchMaxCltv: Int, @@ -61,10 +61,10 @@ case class RouterConf(randomizeRouteSelection: Boolean, case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) -case class RouteParams(randomize: Boolean, maxFeeBaseMsat: Long, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: Int, ratios: Option[WeightRatios]) +case class RouteParams(randomize: Boolean, maxFeeBase: MilliSatoshi, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: Int, ratios: Option[WeightRatios]) case class RouteRequest(source: PublicKey, target: PublicKey, - amountMsat: Long, + amount: MilliSatoshi, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[ChannelDesc] = Set.empty, @@ -758,7 +758,7 @@ object Router { def toFakeUpdate(extraHop: ExtraHop): ChannelUpdate = // the `direction` bit in flags will not be accurate but it doesn't matter because it is not used // what matters is that the `disable` bit is 0 so that this update doesn't get filtered out - ChannelUpdate(signature = ByteVector64.Zeroes, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, Platform.currentTime.milliseconds.toSeconds, messageFlags = 0, channelFlags = 0, extraHop.cltvExpiryDelta, htlcMinimumMsat = 0L, extraHop.feeBaseMsat, extraHop.feeProportionalMillionths, None) + ChannelUpdate(signature = ByteVector64.Zeroes, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, Platform.currentTime.milliseconds.toSeconds, messageFlags = 0, channelFlags = 0, extraHop.cltvExpiryDelta, htlcMinimumMsat = MilliSatoshi(0), MilliSatoshi(extraHop.feeBaseMsat), extraHop.feeProportionalMillionths, None) def toFakeUpdates(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Map[ChannelDesc, ChannelUpdate] = { // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination @@ -981,7 +981,7 @@ object Router { def getDefaultRouteParams(routerConf: RouterConf) = RouteParams( randomize = routerConf.randomizeRouteSelection, - maxFeeBaseMsat = routerConf.searchMaxFeeBaseSat * 1000, // converting sat -> msat + maxFeeBase = routerConf.searchMaxFeeBase.toMilliSatoshi, maxFeePct = routerConf.searchMaxFeePct, routeMaxLength = routerConf.searchMaxRouteLength, routeMaxCltv = routerConf.searchMaxCltv, @@ -1002,7 +1002,7 @@ object Router { * @param g * @param localNodeId * @param targetNodeId - * @param amountMsat the amount that will be sent along this route + * @param amount the amount that will be sent along this route * @param numRoutes the number of shortest-paths to find * @param extraEdges a set of extra edges we want to CONSIDER during the search * @param ignoredEdges a set of extra edges we want to IGNORE during the search @@ -1012,7 +1012,7 @@ object Router { def findRoute(g: DirectedGraph, localNodeId: PublicKey, targetNodeId: PublicKey, - amountMsat: Long, + amount: MilliSatoshi, numRoutes: Int, extraEdges: Set[GraphEdge] = Set.empty, ignoredEdges: Set[ChannelDesc] = Set.empty, @@ -1023,14 +1023,14 @@ object Router { val currentBlockHeight = Globals.blockCount.get() val boundaries: RichWeight => Boolean = { weight => - ((weight.cost - amountMsat) < routeParams.maxFeeBaseMsat || (weight.cost - amountMsat) < (routeParams.maxFeePct * amountMsat)) && + ((weight.cost - amount) < routeParams.maxFeeBase || (weight.cost - amount) < amount * routeParams.maxFeePct.toLong) && weight.length <= routeParams.routeMaxLength && weight.length <= ROUTE_MAX_LENGTH && weight.cltv <= routeParams.routeMaxCltv } - val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList match { + val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList match { case Nil if routeParams.routeMaxLength < ROUTE_MAX_LENGTH => // if not found within the constraints we relax and repeat the search - return findRoute(g, localNodeId, targetNodeId, amountMsat, numRoutes, extraEdges, ignoredEdges, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV)) + return findRoute(g, localNodeId, targetNodeId, amount, numRoutes, extraEdges, ignoredEdges, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV)) case Nil => throw RouteNotFound case routes => routes.find(_.path.size == 1) match { case Some(directRoute) => directRoute :: Nil diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 3448ecfe99..d4b201202d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.transactions +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.wire._ /** @@ -30,8 +31,8 @@ case object OUT extends Direction { def opposite = IN } case class DirectedHtlc(direction: Direction, add: UpdateAddHtlc) -final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocalMsat: Long, toRemoteMsat: Long) { - val totalFunds = toLocalMsat + toRemoteMsat + htlcs.toSeq.map(_.add.amountMsat).sum +final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { + val totalFunds = toLocal + toRemote + htlcs.toSeq.map(_.add.amountMsat).sum } object CommitmentSpec { @@ -43,16 +44,16 @@ object CommitmentSpec { def addHtlc(spec: CommitmentSpec, direction: Direction, update: UpdateAddHtlc): CommitmentSpec = { val htlc = DirectedHtlc(direction, update) direction match { - case OUT => spec.copy(toLocalMsat = spec.toLocalMsat - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) - case IN => spec.copy(toRemoteMsat = spec.toRemoteMsat - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) + case OUT => spec.copy(toLocal = spec.toLocal - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) + case IN => spec.copy(toRemote = spec.toRemote - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) } } // OUT means we are sending an UpdateFulfillHtlc message which means that we are fulfilling an HTLC that they sent def fulfillHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = { spec.htlcs.find(htlc => htlc.direction != direction && htlc.add.id == htlcId) match { - case Some(htlc) if direction == OUT => spec.copy(toLocalMsat = spec.toLocalMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) - case Some(htlc) if direction == IN => spec.copy(toRemoteMsat = spec.toRemoteMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == OUT => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == IN => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) case None => throw new RuntimeException(s"cannot find htlc id=${htlcId}") } } @@ -60,8 +61,8 @@ object CommitmentSpec { // OUT means we are sending an UpdateFailHtlc message which means that we are failing an HTLC that they sent def failHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = { spec.htlcs.find(htlc => htlc.direction != direction && htlc.add.id == htlcId) match { - case Some(htlc) if direction == OUT => spec.copy(toRemoteMsat = spec.toRemoteMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) - case Some(htlc) if direction == IN => spec.copy(toLocalMsat = spec.toLocalMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == OUT => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == IN => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) case None => throw new RuntimeException(s"cannot find htlc id=${htlcId}") } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 0febeaab9e..9ea53c8a1b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -21,7 +21,8 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.SigVersion._ -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering, MilliSatoshi, OutPoint, Protocol, SIGHASH_ALL, Satoshi, Script, ScriptElt, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut, millisatoshi2satoshi} +import fr.acinq.bitcoin._ +import fr.acinq.eclair._ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.UpdateAddHtlc import scodec.bits.ByteVector @@ -121,7 +122,7 @@ object Transactions { val htlcTimeoutFee = weight2fee(spec.feeratePerKw, htlcTimeoutWeight) spec.htlcs .filter(_.direction == OUT) - .filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcTimeoutFee)) + .filter(htlc => htlc.add.amountMsat >= (dustLimit + htlcTimeoutFee).toMilliSatoshi) .toSeq } @@ -129,7 +130,7 @@ object Transactions { val htlcSuccessFee = weight2fee(spec.feeratePerKw, htlcSuccessWeight) spec.htlcs .filter(_.direction == IN) - .filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcSuccessFee)) + .filter(htlc => htlc.add.amountMsat >= (dustLimit + htlcSuccessFee).toMilliSatoshi) .toSeq } @@ -190,18 +191,18 @@ object Transactions { val commitFee = commitTxFee(localDustLimit, spec) val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - commitFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat))) + (spec.toLocal.truncateToSatoshi - commitFee, spec.toRemote.truncateToSatoshi) } else { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - commitFee) + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitFee) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway val toLocalDelayedOutput_opt = if (toLocalAmount >= localDustLimit) Some(TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))) else None val toRemoteOutput_opt = if (toRemoteAmount >= localDustLimit) Some(TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey))) else None val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes))))) + .map(htlc => TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes))))) val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry)))) + .map(htlc => TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry)))) val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, locktime) = encodeTxNumber(txnumber) @@ -218,8 +219,8 @@ object Transactions { val fee = weight2fee(feeratePerKw, htlcTimeoutWeight) val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) - val amount = MilliSatoshi(htlc.amountMsat) - fee + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { throw AmountBelowDustLimit } @@ -235,8 +236,8 @@ object Transactions { val fee = weight2fee(feeratePerKw, htlcSuccessWeight) val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) - val amount = MilliSatoshi(htlc.amountMsat) - fee + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { throw AmountBelowDustLimit } @@ -266,7 +267,7 @@ object Transactions { def makeClaimHtlcSuccessTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcSuccessTx = { val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) val tx = Transaction( @@ -289,7 +290,7 @@ object Transactions { def makeClaimHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = { val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) // unsigned tx @@ -447,9 +448,9 @@ object Transactions { require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - closingFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat))) + (spec.toLocal.truncateToSatoshi - closingFee, spec.toRemote.truncateToSatoshi) } else { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - closingFee) + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - closingFee) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway val toLocalOutput_opt = if (toLocalAmount >= dustLimit) Some(TxOut(toLocalAmount, localScriptPubKey)) else None diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 97937c05de..df95cd7972 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 @@ -61,10 +61,10 @@ object ChannelCodecs extends Logging { val localParamsCodec: Codec[LocalParams] = ( ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: + ("channelReserve" | satoshi) :: + ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | uint16) :: ("maxAcceptedHtlcs" | uint16) :: ("isFunder" | bool) :: @@ -74,10 +74,10 @@ object ChannelCodecs extends Logging { val remoteParamsCodec: Codec[RemoteParams] = ( ("nodeId" | publicKey) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: + ("channelReserve" | satoshi) :: + ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | uint16) :: ("maxAcceptedHtlcs" | uint16) :: ("fundingPubKey" | publicKey) :: @@ -105,8 +105,8 @@ object ChannelCodecs extends Logging { val commitmentSpecCodec: Codec[CommitmentSpec] = ( ("htlcs" | setCodec(htlcCodec)) :: ("feeratePerKw" | uint32) :: - ("toLocalMsat" | uint64overflow) :: - ("toRemoteMsat" | uint64overflow)).as[CommitmentSpec] + ("toLocal" | millisatoshi) :: + ("toRemote" | millisatoshi)).as[CommitmentSpec] val outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) @@ -186,8 +186,8 @@ object ChannelCodecs extends Logging { val relayedCodec: Codec[Relayed] = ( ("originChannelId" | bytes32) :: ("originHtlcId" | int64) :: - ("amountMsatIn" | uint64overflow) :: - ("amountMsatOut" | uint64overflow)).as[Relayed] + ("amountIn" | millisatoshi) :: + ("amountOut" | millisatoshi)).as[Relayed] // this is for backward compatibility to handle legacy payments that didn't have identifiers val UNKNOWN_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala index e56a3645fd..fc749c8fe8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -52,9 +52,11 @@ object CommonCodecs { // this codec can be safely used for values < 2^63 and will fail otherwise // (for something smarter see https://github.com/yzernik/bitcoin-scodec/blob/master/src/main/scala/io/github/yzernik/bitcoinscodec/structures/UInt64.scala) val uint64overflow: Codec[Long] = int64.narrow(l => if (l >= 0) Attempt.Successful(l) else Attempt.failure(Err(s"overflow for value $l")), l => l) - val uint64: Codec[UInt64] = bytes(8).xmap(b => UInt64(b), a => a.toByteVector.padLeft(8)) + val satoshi: Codec[Satoshi] = uint64overflow.xmapc(l => Satoshi(l))(_.toLong) + val millisatoshi: Codec[MilliSatoshi] = uint64overflow.xmapc(l => MilliSatoshi(l))(_.amount) + /** * We impose a minimal encoding on some values (such as varint and truncated int) to ensure that signed hashes can be * re-computed correctly. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index a7cee716dd..db816407be 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.wire.CommonCodecs.{sha256, uint64overflow} +import fr.acinq.eclair.wire.CommonCodecs.{sha256, millisatoshi} import fr.acinq.eclair.wire.LightningMessageCodecs.{channelUpdateCodec, lightningMessageCodec} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -47,16 +48,16 @@ case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def m case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } case object UnknownNextPeer extends Perm { def message = "processing node does not know the next peer in the route" } -case class AmountBelowMinimum(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment amount was below the minimum required by the channel" } -case class FeeInsufficient(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment fee was below the minimum required by the channel" } +case class AmountBelowMinimum(amount: MilliSatoshi, update: ChannelUpdate) extends Update { def message = s"payment amount was below the minimum required by the channel" } +case class FeeInsufficient(amount: MilliSatoshi, update: ChannelUpdate) extends Update { def message = s"payment fee was below the minimum required by the channel" } case class ChannelDisabled(messageFlags: Byte, channelFlags: Byte, update: ChannelUpdate) extends Update { def message = "channel is currently disabled" } case class IncorrectCltvExpiry(expiry: Long, update: ChannelUpdate) extends Update { def message = "payment expiry doesn't match the value in the onion" } -case class IncorrectOrUnknownPaymentDetails(amountMsat: Long) extends Perm { def message = "incorrect payment amount or unknown payment hash" } +case class IncorrectOrUnknownPaymentDetails(amount: MilliSatoshi) extends Perm { def message = "incorrect payment amount or unknown payment hash" } case object IncorrectPaymentAmount extends Perm { def message = "payment amount is incorrect" } case class ExpiryTooSoon(update: ChannelUpdate) extends Update { def message = "payment expiry is too close to the current block height for safe handling by the relaying node" } case object FinalExpiryTooSoon extends FailureMessage { def message = "payment expiry is too close to the current block height for safe handling by the final node" } case class FinalIncorrectCltvExpiry(expiry: Long) extends FailureMessage { def message = "payment expiry doesn't match the value in the onion" } -case class FinalIncorrectHtlcAmount(amountMsat: Long) extends FailureMessage { def message = "payment amount is incorrect in the final htlc" } +case class FinalIncorrectHtlcAmount(amount: MilliSatoshi) extends FailureMessage { def message = "payment amount is incorrect in the final htlc" } case object ExpiryTooFar extends FailureMessage { def message = "payment expiry is too far in the future" } // @formatter:on @@ -85,16 +86,16 @@ object FailureMessageCodecs { .typecase(PERM | 8, provide(PermanentChannelFailure)) .typecase(PERM | 9, provide(RequiredChannelFeatureMissing)) .typecase(PERM | 10, provide(UnknownNextPeer)) - .typecase(UPDATE | 11, (("amountMsat" | uint64overflow) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[AmountBelowMinimum]) - .typecase(UPDATE | 12, (("amountMsat" | uint64overflow) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient]) + .typecase(UPDATE | 11, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[AmountBelowMinimum]) + .typecase(UPDATE | 12, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient]) .typecase(UPDATE | 13, (("expiry" | uint32) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[IncorrectCltvExpiry]) .typecase(UPDATE | 14, ("channelUpdate" | channelUpdateWithLengthCodec).as[ExpiryTooSoon]) .typecase(UPDATE | 20, (("messageFlags" | byte) :: ("channelFlags" | byte) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[ChannelDisabled]) - .typecase(PERM | 15, ("amountMsat" | withDefaultValue(optional(bitsRemaining, uint64overflow), 0L)).as[IncorrectOrUnknownPaymentDetails]) + .typecase(PERM | 15, ("amountMsat" | withDefaultValue(optional(bitsRemaining, millisatoshi), MilliSatoshi(0))).as[IncorrectOrUnknownPaymentDetails]) .typecase(PERM | 16, provide(IncorrectPaymentAmount)) .typecase(17, provide(FinalExpiryTooSoon)) .typecase(18, ("expiry" | uint32).as[FinalIncorrectCltvExpiry]) - .typecase(19, ("amountMsat" | uint64overflow).as[FinalIncorrectHtlcAmount]) + .typecase(19, ("amountMsat" | millisatoshi).as[FinalIncorrectHtlcAmount]) .typecase(21, provide(ExpiryTooFar)) /** 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 cad0f8bd7b..62f349f7f5 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,7 +16,7 @@ package fr.acinq.eclair.wire -import fr.acinq.eclair.{UInt64, wire} +import fr.acinq.eclair.{MilliSatoshi, wire} import fr.acinq.eclair.wire.CommonCodecs._ import scodec.Codec import scodec.codecs._ @@ -51,12 +51,12 @@ object LightningMessageCodecs { val openChannelCodec: Codec[OpenChannel] = ( ("chainHash" | bytes32) :: ("temporaryChannelId" | bytes32) :: - ("fundingSatoshis" | uint64overflow) :: - ("pushMsat" | uint64overflow) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("fundingSatoshis" | satoshi) :: + ("pushMsat" | millisatoshi) :: + ("dustLimitSatoshis" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: + ("channelReserveSatoshis" | satoshi) :: + ("htlcMinimumMsat" | millisatoshi) :: ("feeratePerKw" | uint32) :: ("toSelfDelay" | uint16) :: ("maxAcceptedHtlcs" | uint16) :: @@ -70,10 +70,10 @@ object LightningMessageCodecs { val acceptChannelCodec: Codec[AcceptChannel] = ( ("temporaryChannelId" | bytes32) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("dustLimitSatoshis" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: + ("channelReserveSatoshis" | satoshi) :: + ("htlcMinimumMsat" | millisatoshi) :: ("minimumDepth" | uint32) :: ("toSelfDelay" | uint16) :: ("maxAcceptedHtlcs" | uint16) :: @@ -104,13 +104,13 @@ object LightningMessageCodecs { val closingSignedCodec: Codec[ClosingSigned] = ( ("channelId" | bytes32) :: - ("feeSatoshis" | uint64overflow) :: + ("feeSatoshis" | satoshi) :: ("signature" | bytes64)).as[ClosingSigned] val updateAddHtlcCodec: Codec[UpdateAddHtlc] = ( ("channelId" | bytes32) :: ("id" | uint64overflow) :: - ("amountMsat" | uint64overflow) :: + ("amountMsat" | millisatoshi) :: ("paymentHash" | bytes32) :: ("expiry" | uint32) :: ("onionRoutingPacket" | OnionCodecs.paymentOnionPacketCodec)).as[UpdateAddHtlc] @@ -187,10 +187,10 @@ object LightningMessageCodecs { (("messageFlags" | byte) >>:~ { messageFlags => ("channelFlags" | byte) :: ("cltvExpiryDelta" | uint16) :: - ("htlcMinimumMsat" | uint64overflow) :: - ("feeBaseMsat" | uint32) :: + ("htlcMinimumMsat" | millisatoshi) :: + ("feeBaseMsat" | uint32.xmapc(l => MilliSatoshi(l))(_.amount)) :: ("feeProportionalMillionths" | uint32) :: - ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, uint64overflow)) + ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, millisatoshi)) }) @@ -201,10 +201,10 @@ object LightningMessageCodecs { (("messageFlags" | byte) >>:~ { messageFlags => ("channelFlags" | byte) :: ("cltvExpiryDelta" | uint16) :: - ("htlcMinimumMsat" | uint64overflow) :: - ("feeBaseMsat" | uint32) :: + ("htlcMinimumMsat" | millisatoshi) :: + ("feeBaseMsat" | uint32.xmapc(l => MilliSatoshi(l))(_.amount)) :: ("feeProportionalMillionths" | uint32) :: - ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, uint64overflow)) :: + ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, millisatoshi)) :: ("unknownFields" | bytes) }) 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 b7f7d129b2..b84e5e3972 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 @@ -20,9 +20,9 @@ import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import java.nio.charset.StandardCharsets import com.google.common.base.Charsets +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector import scala.util.Try @@ -68,12 +68,12 @@ case class ChannelReestablish(channelId: ByteVector32, case class OpenChannel(chainHash: ByteVector32, temporaryChannelId: ByteVector32, - fundingSatoshis: Long, - pushMsat: Long, - dustLimitSatoshis: Long, + fundingSatoshis: Satoshi, + pushMsat: MilliSatoshi, + dustLimitSatoshis: Satoshi, maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, + channelReserveSatoshis: Satoshi, + htlcMinimumMsat: MilliSatoshi, feeratePerKw: Long, toSelfDelay: Int, maxAcceptedHtlcs: Int, @@ -86,10 +86,10 @@ case class OpenChannel(chainHash: ByteVector32, channelFlags: Byte) extends ChannelMessage with HasTemporaryChannelId with HasChainHash case class AcceptChannel(temporaryChannelId: ByteVector32, - dustLimitSatoshis: Long, + dustLimitSatoshis: Satoshi, maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, + channelReserveSatoshis: Satoshi, + htlcMinimumMsat: MilliSatoshi, minimumDepth: Long, toSelfDelay: Int, maxAcceptedHtlcs: Int, @@ -115,12 +115,12 @@ case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector) extends ChannelMessage with HasChannelId case class ClosingSigned(channelId: ByteVector32, - feeSatoshis: Long, + feeSatoshis: Satoshi, signature: ByteVector64) extends ChannelMessage with HasChannelId case class UpdateAddHtlc(channelId: ByteVector32, id: Long, - amountMsat: Long, + amountMsat: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: Long, onionRoutingPacket: OnionRoutingPacket) extends HtlcMessage with UpdateMessage with HasChannelId @@ -217,10 +217,10 @@ case class ChannelUpdate(signature: ByteVector64, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: Int, - htlcMinimumMsat: Long, - feeBaseMsat: Long, + htlcMinimumMsat: MilliSatoshi, + feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, - htlcMaximumMsat: Option[Long], + htlcMaximumMsat: Option[MilliSatoshi], unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with HasTimestamp with HasChainHash { require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala index 32d942f547..716f11b16e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.crypto.Sphinx import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -33,7 +33,7 @@ case class OnionRoutingPacket(version: Int, hmac: ByteVector32) case class PerHopPayload(shortChannelId: ShortChannelId, - amtToForward: Long, + amtToForward: MilliSatoshi, outgoingCltvValue: Long) object OnionCodecs { @@ -49,7 +49,7 @@ object OnionCodecs { val perHopPayloadCodec: Codec[PerHopPayload] = ( ("realm" | constant(ByteVector.fromByte(0))) :: ("short_channel_id" | CommonCodecs.shortchannelid) :: - ("amt_to_forward" | CommonCodecs.uint64overflow) :: + ("amt_to_forward" | CommonCodecs.millisatoshi) :: ("outgoing_cltv_value" | uint32) :: ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] diff --git a/eclair-core/src/test/resources/api/usablebalances b/eclair-core/src/test/resources/api/usablebalances index edbd4e5a97..c1ef6b4920 100644 --- a/eclair-core/src/test/resources/api/usablebalances +++ b/eclair-core/src/test/resources/api/usablebalances @@ -1 +1 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSendMsat":100000000,"canReceiveMsat":20000000,"isPublic":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSendMsat":400000000,"canReceiveMsat":30000000,"isPublic":false}] \ No newline at end of file +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false}] \ No newline at end of file diff --git a/eclair-core/src/test/resources/scenarii/01-offer1.script.expected b/eclair-core/src/test/resources/scenarii/01-offer1.script.expected index c3444f106f..5680bafccc 100644 --- a/eclair-core/src/test/resources/scenarii/01-offer1.script.expected +++ b/eclair-core/src/test/resources/scenarii/01-offer1.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 1: - Offered htlcs: (0,1000000) + Offered htlcs: (0,MilliSatoshi(1000000)) Received htlcs: - Balance us: 999000000 - Balance them: 1000000000 + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(1000000000) Fee rate: 10000 REMOTE COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) - Balance us: 1000000000 - Balance them: 999000000 + Received htlcs: (0,MilliSatoshi(1000000)) + Balance us: MilliSatoshi(1000000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) - Balance us: 1000000000 - Balance them: 999000000 + Received htlcs: (0,MilliSatoshi(1000000)) + Balance us: MilliSatoshi(1000000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 REMOTE COMMITS: Commit 1: - Offered htlcs: (0,1000000) + Offered htlcs: (0,MilliSatoshi(1000000)) Received htlcs: - Balance us: 999000000 - Balance them: 1000000000 + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(1000000000) Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/02-offer2.script.expected b/eclair-core/src/test/resources/scenarii/02-offer2.script.expected index b12ec47413..2d71ea0dea 100644 --- a/eclair-core/src/test/resources/scenarii/02-offer2.script.expected +++ b/eclair-core/src/test/resources/scenarii/02-offer2.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 1: - Offered htlcs: (0,1000000) (1,2000000) + Offered htlcs: (0,MilliSatoshi(1000000)) (1,MilliSatoshi(2000000)) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: MilliSatoshi(997000000) + Balance them: MilliSatoshi(1000000000) Fee rate: 10000 REMOTE COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Received htlcs: (0,MilliSatoshi(1000000)) (1,MilliSatoshi(2000000)) + Balance us: MilliSatoshi(1000000000) + Balance them: MilliSatoshi(997000000) Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Received htlcs: (0,MilliSatoshi(1000000)) (1,MilliSatoshi(2000000)) + Balance us: MilliSatoshi(1000000000) + Balance them: MilliSatoshi(997000000) Fee rate: 10000 REMOTE COMMITS: Commit 1: - Offered htlcs: (0,1000000) (1,2000000) + Offered htlcs: (0,MilliSatoshi(1000000)) (1,MilliSatoshi(2000000)) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: MilliSatoshi(997000000) + Balance them: MilliSatoshi(1000000000) Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected b/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected index 1ffcada1d7..3c3fff0fe3 100644 --- a/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected +++ b/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected @@ -3,28 +3,28 @@ LOCAL COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 999000000 - Balance them: 1001000000 + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(1001000000) Fee rate: 10000 REMOTE COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 1001000000 - Balance them: 999000000 + Balance us: MilliSatoshi(1001000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 1001000000 - Balance them: 999000000 + Balance us: MilliSatoshi(1001000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 REMOTE COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 999000000 - Balance them: 1001000000 + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(1001000000) Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected b/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected index f9eacd72d4..965efa4bc8 100644 --- a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected +++ b/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected @@ -3,28 +3,28 @@ LOCAL COMMITS: Commit 1: Offered htlcs: (0,1000000) (1,2000000) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: MilliSatoshi(997000000) + Balance them: MilliSatoshi(1000000000) Fee rate: 10000 REMOTE COMMITS: Commit 2: Offered htlcs: Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Balance us: MilliSatoshi(1000000000) + Balance them: MilliSatoshi(997000000) Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: Offered htlcs: Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Balance us: MilliSatoshi(1000000000) + Balance them: MilliSatoshi(997000000) Fee rate: 10000 REMOTE COMMITS: Commit 1: Offered htlcs: (0,1000000) (1,2000000) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: MilliSatoshi(997000000) + Balance them: MilliSatoshi(1000000000) Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected b/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected index dfbeddddb2..51469b487b 100644 --- a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected +++ b/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 1: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,MilliSatoshi(1000000)) + Received htlcs: (0,MilliSatoshi(2000000)) + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(998000000) Fee rate: 10000 REMOTE COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,MilliSatoshi(2000000)) + Received htlcs: (0,MilliSatoshi(1000000)) + Balance us: MilliSatoshi(998000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,MilliSatoshi(2000000)) + Received htlcs: (0,MilliSatoshi(1000000)) + Balance us: MilliSatoshi(998000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 REMOTE COMMITS: Commit 1: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,MilliSatoshi(1000000)) + Received htlcs: (0,MilliSatoshi(2000000)) + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(998000000) Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected b/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected index bf1061fa9a..2c52101d82 100644 --- a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected +++ b/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 2: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,MilliSatoshi(1000000)) + Received htlcs: (0,MilliSatoshi(2000000)) + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(998000000) Fee rate: 10000 REMOTE COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,MilliSatoshi(2000000)) + Received htlcs: (0,MilliSatoshi(1000000)) + Balance us: MilliSatoshi(998000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,MilliSatoshi(2000000)) + Received htlcs: (0,MilliSatoshi(1000000)) + Balance us: MilliSatoshi(998000000) + Balance them: MilliSatoshi(999000000) Fee rate: 10000 REMOTE COMMITS: Commit 2: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,MilliSatoshi(1000000)) + Received htlcs: (0,MilliSatoshi(2000000)) + Balance us: MilliSatoshi(999000000) + Balance them: MilliSatoshi(998000000) Fee rate: 10000 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala index ea294912a5..8e051c2c93 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Btc, MilliBtc, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{Btc, MilliBtc, Satoshi} import org.scalatest.FunSuite 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 2ab1b0b329..728815f5a3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -16,18 +16,14 @@ package fr.acinq.eclair -import java.io.File - import akka.actor.ActorSystem import akka.testkit.{TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.blockchain.TestWallet import fr.acinq.eclair.io.Peer.OpenChannel import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment, SendPayment, SendPaymentToRoute} -import org.scalatest.{Outcome, fixture} import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import org.scalatest.{Matchers, Outcome, fixture} @@ -37,10 +33,8 @@ import fr.acinq.eclair.channel.{CMD_FORCECLOSE, Register} import fr.acinq.eclair.payment.LocalPaymentHandler import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ -import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate import org.mockito.scalatest.IdiomaticMockito - import scala.concurrent.Await import scala.util.{Failure, Success} import scala.concurrent.duration._ @@ -84,12 +78,12 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") // standard conversion - eclair.open(nodeId, fundingSatoshis = 10000000L, pushMsat_opt = None, fundingFeerateSatByte_opt = Some(5), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = Satoshi(10000000L), pushAmount_opt = None, fundingFeerateSatByte_opt = Some(5), flags_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] assert(open.fundingTxFeeratePerKw_opt == Some(1250)) // check that minimum fee rate of 253 sat/bw is used - eclair.open(nodeId, fundingSatoshis = 10000000L, pushMsat_opt = None, fundingFeerateSatByte_opt = Some(1), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = Satoshi(10000000L), pushAmount_opt = None, fundingFeerateSatByte_opt = Some(1), flags_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] assert(open1.fundingTxFeeratePerKw_opt == Some(MinimumFeeratePerKw)) } @@ -100,37 +94,37 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val eclair = new EclairImpl(kit) val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = None) + eclair.send(recipientNodeId = nodeId, amount = MilliSatoshi(123), paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = None) val send = paymentInitiator.expectMsgType[SendPayment] assert(send.targetNodeId == nodeId) - assert(send.amountMsat == 123) + assert(send.amount == MilliSatoshi(123)) assert(send.paymentHash == ByteVector32.Zeroes) assert(send.assistedRoutes == Seq.empty) // with assisted routes val hints = Seq(Seq(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId("569178x2331x1"), feeBaseMsat = 10, feeProportionalMillionths = 1, cltvExpiryDelta = 12))) - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = hints, minFinalCltvExpiry_opt = None) + eclair.send(recipientNodeId = nodeId, amount = MilliSatoshi(123), paymentHash = ByteVector32.Zeroes, assistedRoutes = hints, minFinalCltvExpiry_opt = None) val send1 = paymentInitiator.expectMsgType[SendPayment] assert(send1.targetNodeId == nodeId) - assert(send1.amountMsat == 123) + assert(send1.amount == MilliSatoshi(123)) assert(send1.paymentHash == ByteVector32.Zeroes) assert(send1.assistedRoutes == hints) // with finalCltvExpiry - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = Some(96)) + eclair.send(recipientNodeId = nodeId, amount = MilliSatoshi(123), paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = Some(96)) val send2 = paymentInitiator.expectMsgType[SendPayment] assert(send2.targetNodeId == nodeId) - assert(send2.amountMsat == 123) + assert(send2.amount == MilliSatoshi(123)) assert(send2.paymentHash == ByteVector32.Zeroes) assert(send2.finalCltvExpiry == 96) // with custom route fees parameters - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = None, feeThresholdSat_opt = Some(123), maxFeePct_opt = Some(4.20)) + eclair.send(recipientNodeId = nodeId, amount = MilliSatoshi(123), paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = None, feeThreshold_opt = Some(Satoshi(123)), maxFeePct_opt = Some(4.20)) val send3 = paymentInitiator.expectMsgType[SendPayment] assert(send3.targetNodeId == nodeId) - assert(send3.amountMsat == 123) + assert(send3.amount == MilliSatoshi(123)) assert(send3.paymentHash == ByteVector32.Zeroes) - assert(send3.routeParams.get.maxFeeBaseMsat == 123 * 1000) // conversion sat -> msat + assert(send3.routeParams.get.maxFeeBase == Satoshi(123).toMilliSatoshi) // conversion sat -> msat assert(send3.routeParams.get.maxFeePct == 4.20) } @@ -140,11 +134,11 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val (a, b, c, d, e) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) val updates = List( - makeUpdate(1L, a, b, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 13), - makeUpdate(4L, a, e, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdate(2L, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(3L, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(7L, e, c, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12) + makeUpdate(1L, a, b, feeBase = MilliSatoshi(0), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 13), + makeUpdate(4L, a, e, feeBase = MilliSatoshi(0), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12), + makeUpdate(2L, b, c, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 500), + makeUpdate(3L, c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 500), + makeUpdate(7L, e, c, feeBase = MilliSatoshi(2), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12) ).toMap val eclair = new EclairImpl(kit) @@ -188,15 +182,15 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val fallBackAddressRaw = "muhtvdmsnbQEPFuEmxcChX58fGvXaaUoVt" val eclair = new EclairImpl(kit) - eclair.receive("some desc", Some(123L), Some(456), Some(fallBackAddressRaw), None) + eclair.receive("some desc", Some(MilliSatoshi(123L)), Some(456), Some(fallBackAddressRaw), None) val receive = paymentHandler.expectMsgType[ReceivePayment] - assert(receive.amountMsat_opt == Some(MilliSatoshi(123L))) + assert(receive.amount_opt == Some(MilliSatoshi(123L))) assert(receive.expirySeconds_opt == Some(456)) assert(receive.fallbackAddress == Some(fallBackAddressRaw)) // try with wrong address format - assertThrows[IllegalArgumentException](eclair.receive("some desc", Some(123L), Some(456), Some("wassa wassa"), None)) + assertThrows[IllegalArgumentException](eclair.receive("some desc", Some(MilliSatoshi(123L)), Some(456), Some("wassa wassa"), None)) } test("passing a payment_preimage to /createinvoice should result in an invoice with payment_hash=H(payment_preimage)") { fixture => @@ -206,7 +200,7 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val eclair = new EclairImpl(kitWithPaymentHandler) val paymentPreimage = randomBytes32 - val fResp = eclair.receive(description = "some desc", amountMsat_opt = None, expire_opt = None, fallbackAddress_opt = None, paymentPreimage_opt = Some(paymentPreimage)) + val fResp = eclair.receive(description = "some desc", amount_opt = None, expire_opt = None, fallbackAddress_opt = None, paymentPreimage_opt = Some(paymentPreimage)) awaitCond({ fResp.value match { case Some(Success(pr)) => pr.paymentHash == Crypto.sha256(paymentPreimage) @@ -252,12 +246,12 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val route = Seq(PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87")) val eclair = new EclairImpl(kit) - eclair.sendToRoute(route, 1234, ByteVector32.One, 123) + eclair.sendToRoute(route, MilliSatoshi(1234), ByteVector32.One, 123) val send = paymentInitiator.expectMsgType[SendPaymentToRoute] assert(send.hops == route) - assert(send.amountMsat == 1234) + assert(send.amount == MilliSatoshi(1234)) assert(send.finalCltvExpiry == 123) assert(send.paymentHash == ByteVector32.One) } 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 3f5e0b3744..9c340e5a8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair import java.sql.{Connection, DriverManager} import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, Script} +import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, Script} import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.crypto.LocalKeyManager @@ -36,8 +36,8 @@ import scala.concurrent.duration._ */ object TestConstants { - val fundingSatoshis = 1000000L - val pushMsat = 200000000L + val fundingSatoshis = Satoshi(1000000L) + val pushMsat = MilliSatoshi(200000000L) val feeratePerKw = 10000L val emptyOnionPacket = wire.OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) @@ -70,7 +70,7 @@ object TestConstants { globalFeatures = ByteVector.empty, localFeatures = ByteVector(0), overrideFeatures = Map.empty, - dustLimitSatoshis = 1100, + dustLimit = Satoshi(1100), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 2, 6), feeEstimator = new TestFeeEstimator, @@ -81,11 +81,11 @@ object TestConstants { maxAcceptedHtlcs = 100, expiryDeltaBlocks = 144, fulfillSafetyBeforeTimeoutBlocks = 6, - htlcMinimumMsat = 0, + htlcMinimum = MilliSatoshi(0), minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - feeBaseMsat = 546000, + feeBase = MilliSatoshi(546000), feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, @@ -101,12 +101,12 @@ object TestConstants { channelFlags = 1, watcherType = BITCOIND, paymentRequestExpiry = 1 hour, - minFundingSatoshis = 1000L, + minFundingSatoshis = Satoshi(1000L), routerConf = RouterConf( randomizeRouteSelection = false, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, - searchMaxFeeBaseSat = 21, + searchMaxFeeBase = Satoshi(21), searchMaxFeePct = 0.03, searchMaxCltv = 2016, searchMaxRouteLength = 20, @@ -124,7 +124,7 @@ object TestConstants { defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), isFunder = true, fundingSatoshis).copy( - channelReserveSatoshis = 10000 // Bob will need to keep that much satoshis as direct payment + channelReserve = Satoshi(10000) // Bob will need to keep that much satoshis as direct payment ) } @@ -140,7 +140,7 @@ object TestConstants { globalFeatures = ByteVector.empty, localFeatures = ByteVector.empty, // no announcement overrideFeatures = Map.empty, - dustLimitSatoshis = 1000, + dustLimit = Satoshi(1000), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 2, 6), feeEstimator = new TestFeeEstimator, @@ -151,11 +151,11 @@ object TestConstants { maxAcceptedHtlcs = 30, expiryDeltaBlocks = 144, fulfillSafetyBeforeTimeoutBlocks = 6, - htlcMinimumMsat = 1000, + htlcMinimum = MilliSatoshi(1000), minDepthBlocks = 3, toRemoteDelayBlocks = 144, maxToLocalDelayBlocks = 1000, - feeBaseMsat = 546000, + feeBase = MilliSatoshi(546000), feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, @@ -171,12 +171,12 @@ object TestConstants { channelFlags = 1, watcherType = BITCOIND, paymentRequestExpiry = 1 hour, - minFundingSatoshis = 1000L, + minFundingSatoshis = Satoshi(1000L), routerConf = RouterConf( randomizeRouteSelection = false, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, - searchMaxFeeBaseSat = 21, + searchMaxFeeBase = Satoshi(21), searchMaxFeePct = 0.03, searchMaxCltv = 2016, searchMaxRouteLength = 20, @@ -194,7 +194,7 @@ object TestConstants { defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), isFunder = false, fundingSatoshis).copy( - channelReserveSatoshis = 20000 // Alice will need to keep that much satoshis as direct payment + channelReserve = Satoshi(20000) // Alice will need to keep that much satoshis as direct payment ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 4c3f5d9e9d..87fbffeefe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -27,7 +27,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe import akka.stream.ActorMaterializer import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.io.NodeURI @@ -39,6 +39,7 @@ import org.json4s.jackson.Serialization import org.mockito.scalatest.IdiomaticMockito import org.scalatest.{FunSuite, Matchers} import scodec.bits._ + import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source @@ -147,8 +148,8 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock val eclair = mock[Eclair] val mockService = new MockService(eclair) eclair.usableBalances()(any[Timeout]) returns Future.successful(List( - UsableBalances(canSendMsat = 100000000, canReceiveMsat = 20000000, shortChannelId = ShortChannelId(1), remoteNodeId = TestConstants.Alice.keyManager.nodeKey.publicKey, isPublic = true), - UsableBalances(canSendMsat = 400000000, canReceiveMsat = 30000000, shortChannelId = ShortChannelId(2), remoteNodeId = TestConstants.Alice.keyManager.nodeKey.publicKey, isPublic = false) + UsableBalances(canSend = MilliSatoshi(100000000), canReceive = MilliSatoshi(20000000), shortChannelId = ShortChannelId(1), remoteNodeId = TestConstants.Alice.keyManager.nodeKey.publicKey, isPublic = true), + UsableBalances(canSend = MilliSatoshi(400000000), canReceive = MilliSatoshi(30000000), shortChannelId = ShortChannelId(2), remoteNodeId = TestConstants.Alice.keyManager.nodeKey.publicKey, isPublic = false) )) Post("/usablebalances") ~> @@ -269,7 +270,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock check { assert(handled) assert(status == OK) - eclair.send(any, 1258000, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) + eclair.send(any, MilliSatoshi(1258000), any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } @@ -279,7 +280,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock check { assert(handled) assert(status == OK) - eclair.send(any, 123, any, any, any, any, Some(112233), Some(2.34))(any[Timeout]).wasCalled(once) + eclair.send(any, MilliSatoshi(123), any, any, any, any, Some(Satoshi(112233)), Some(2.34))(any[Timeout]).wasCalled(once) } } @@ -311,7 +312,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock val jsonNodes = serialization.write(expectedRoute) val eclair = mock[Eclair] - eclair.sendToRoute(any[List[PublicKey]], anyLong, any[ByteVector32], anyLong)(any[Timeout]) returns Future.successful(paymentUUID) + eclair.sendToRoute(any[List[PublicKey]], any[MilliSatoshi], any[ByteVector32], anyLong)(any[Timeout]) returns Future.successful(paymentUUID) val mockService = new MockService(eclair) Post("/sendtoroute", FormData("route" -> jsonNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.Zeroes.toHex, "finalCltvExpiry" -> "190").toEntity) ~> @@ -322,7 +323,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock assert(handled) assert(status == OK) assert(entityAs[String] == "\""+rawUUID+"\"") - eclair.sendToRoute(expectedRoute, 1234, ByteVector32.Zeroes, 190)(any[Timeout]).wasCalled(once) + eclair.sendToRoute(expectedRoute, MilliSatoshi(1234), ByteVector32.Zeroes, 190)(any[Timeout]).wasCalled(once) } // this test uses CSV encoded route @@ -334,7 +335,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock assert(handled) assert(status == OK) assert(entityAs[String] == "\""+rawUUID+"\"") - eclair.sendToRoute(expectedRoute, 1234, ByteVector32.One, 190)(any[Timeout]).wasCalled(once) + eclair.sendToRoute(expectedRoute, MilliSatoshi(1234), ByteVector32.One, 190)(any[Timeout]).wasCalled(once) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 4313c853e6..38a6b6d7cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.api import java.net.InetAddress import java.util.UUID -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, OutPoint, Transaction} +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain} import fr.acinq.eclair.api.JsonSupport.CustomTypeHints diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index 3af10b6426..50fb3666ef 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -257,7 +257,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val tx1 = Transaction.read(signedTx1) // let's then generate another tx that double spends the first one val inputs = tx1.txIn.map(txIn => Map("txid" -> txIn.outPoint.txid.toString, "vout" -> txIn.outPoint.index)).toArray - bitcoinClient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount.toLong).sum * 1.0 / 1e8)).pipeTo(sender.ref) + bitcoinClient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount).sum.toLong * 1.0 / 1e8)).pipeTo(sender.ref) val JString(unsignedtx2) = sender.expectMsgType[JValue] bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx2).pipeTo(sender.ref) val JString(signedTx2) = sender.expectMsgType[JValue] \ "hex" 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 d44d98dc94..188ceada39 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 @@ -22,7 +22,7 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ @@ -93,7 +93,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi def buildCmdAdd(paymentHash: ByteVector32, dest: PublicKey) = { // allow overpaying (no more than 2 times the required amount) - val amount = requiredAmount + Random.nextInt(requiredAmount) + val amount = MilliSatoshi(requiredAmount + Random.nextInt(requiredAmount)) val expiry = Globals.blockCount.get().toInt + Channel.MIN_CLTV_EXPIRY + 1 PaymentLifecycle.buildCommand(UUID.randomUUID(), amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1 } 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 9ff1329b8e..08aa1f88d6 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 @@ -51,7 +51,7 @@ class ThroughputSpec extends FunSuite { case ('add, tgt: ActorRef) => val r = randomBytes32 val h = Crypto.sha256(r) - tgt ! CMD_ADD_HTLC(1, h, 1, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + tgt ! CMD_ADD_HTLC(MilliSatoshi(1), h, 1, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) context.become(run(h2r + (h -> r))) case ('sig, tgt: ActorRef) => tgt ! CMD_SIGN 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 ac21eeba31..e614c054d7 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 @@ -20,6 +20,7 @@ import java.util.UUID import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.eclair import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeTargets @@ -29,6 +30,7 @@ import fr.acinq.eclair.payment.PaymentLifecycle import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, TestConstants, randomBytes32} +import fr.acinq.eclair._ /** * Created by PM on 23/08/2016. @@ -66,7 +68,7 @@ trait StateTestsHelperMethods extends TestKitBase { tags: Set[String] = Set.empty): Unit = { import setup._ val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty - val pushMsat = if (tags.contains("no_push_msat")) 0 else TestConstants.pushMsat + val pushMsat = if (tags.contains("no_push_msat")) MilliSatoshi(0) else TestConstants.pushMsat val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) @@ -98,19 +100,19 @@ trait StateTestsHelperMethods extends TestKitBase { bob2blockchain.expectMsgType[WatchConfirmed] // deeply buried awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSendMsat == math.max(pushMsat - TestConstants.Alice.channelParams.channelReserveSatoshis * 1000, 0)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == maxOf(pushMsat - TestConstants.Alice.channelParams.channelReserve.toMilliSatoshi, MilliSatoshi(0))) // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] channelUpdateListener.expectMsgType[LocalChannelUpdate] } - def addHtlc(amountMsat: Int, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): (ByteVector32, UpdateAddHtlc) = { + def addHtlc(amount: MilliSatoshi, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): (ByteVector32, UpdateAddHtlc) = { val R: ByteVector32 = randomBytes32 val H: ByteVector32 = Crypto.sha256(R) val sender = TestProbe() val receiverPubkey = r.underlyingActor.nodeParams.nodeId val expiry = 400144 - val cmd = PaymentLifecycle.buildCommand(UUID.randomUUID, amountMsat, expiry, H, Hop(null, receiverPubkey, null) :: Nil)._1.copy(commit = false) + val cmd = PaymentLifecycle.buildCommand(UUID.randomUUID, amount, expiry, H, Hop(null, receiverPubkey, null) :: Nil)._1.copy(commit = false) sender.send(s, cmd) sender.expectMsg("ok") val htlc = s2r.expectMsgType[UpdateAddHtlc] 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 b71fd9de45..980384bc58 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 @@ -83,7 +83,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // we don't want their dust limit to be below 546 - val lowDustLimitSatoshis = 545 + val lowDustLimitSatoshis = Satoshi(545) alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis) val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUSTLIMIT).getMessage)) @@ -104,7 +104,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // 30% is huge, recommended ratio is 1% - val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong + val reserveTooHigh = Satoshi((0.3 * TestConstants.fundingSatoshis.toLong).toLong) alice ! accept.copy(channelReserveSatoshis = reserveTooHigh) val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) @@ -114,7 +114,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (reserve below dust limit)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - val reserveTooSmall = accept.dustLimitSatoshis - 1 + val reserveTooSmall = accept.dustLimitSatoshis - Satoshi(1) alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage)) @@ -125,7 +125,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent - val reserveTooSmall = open.dustLimitSatoshis - 1 + val reserveTooSmall = open.dustLimitSatoshis - Satoshi(1) alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage)) @@ -136,7 +136,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent - val dustTooBig = open.channelReserveSatoshis + 1 + val dustTooBig = open.channelReserveSatoshis + Satoshi(1) alice ! accept.copy(dustLimitSatoshis = dustTooBig) val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage)) 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 6b74dfd654..549ee6f692 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 @@ -17,12 +17,12 @@ package fr.acinq.eclair.channel.states.a import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, ByteVector32} +import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{Error, Init, OpenChannel} -import fr.acinq.eclair.{TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{MilliSatoshi, TestConstants, TestkitBaseClass, ToMilliSatoshiConversion} import org.scalatest.Outcome import scala.concurrent.duration._ @@ -69,20 +69,20 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (funding too low)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val lowFundingMsat = 100 - bob ! open.copy(fundingSatoshis = lowFundingMsat) + val lowFunding = Satoshi(100) + bob ! open.copy(fundingSatoshis = lowFunding) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage)) + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING).getMessage)) awaitCond(bob.stateName == CLOSED) } test("recv OpenChannel (funding too high)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val highFundingMsat = 100000000 + val highFundingMsat = Satoshi(100000000) bob ! open.copy(fundingSatoshis = highFundingMsat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage)) + assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING).getMessage).toAscii) awaitCond(bob.stateName == CLOSED) } @@ -99,10 +99,10 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (invalid push_msat)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val invalidPushMsat = 100000000000L + val invalidPushMsat = MilliSatoshi(100000000000L) bob ! open.copy(pushMsat = invalidPushMsat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, 1000 * open.fundingSatoshis).getMessage)) + assert(error === Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, open.fundingSatoshis.toMilliSatoshi).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -120,7 +120,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper import f._ val open = alice2bob.expectMsgType[OpenChannel] // 30% is huge, recommended ratio is 1% - val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong + val reserveTooHigh = Satoshi((0.3 * TestConstants.fundingSatoshis.toLong).toLong) bob ! open.copy(channelReserveSatoshis = reserveTooHigh) val error = bob2alice.expectMsgType[Error] assert(error === Error(open.temporaryChannelId, ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) @@ -155,7 +155,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (reserve below dust)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val reserveTooSmall = open.dustLimitSatoshis - 1 + val reserveTooSmall = open.dustLimitSatoshis - Satoshi(1) bob ! open.copy(channelReserveSatoshis = reserveTooSmall) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id @@ -166,12 +166,12 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (toLocal + toRemote below reserve)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val fundingSatoshis = open.channelReserveSatoshis + 499 + val fundingSatoshis = open.channelReserveSatoshis.toLong + 499 val pushMsat = 500 * 1000 - bob ! open.copy(fundingSatoshis = fundingSatoshis, pushMsat = pushMsat) + bob ! open.copy(fundingSatoshis = Satoshi(fundingSatoshis), pushMsat = MilliSatoshi(pushMsat)) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, ChannelReserveNotMet(open.temporaryChannelId, 500 * 1000, (open.channelReserveSatoshis - 1) * 1000, open.channelReserveSatoshis).getMessage)) + assert(error === Error(open.temporaryChannelId, ChannelReserveNotMet(open.temporaryChannelId, Satoshi(500).toMilliSatoshi, (open.channelReserveSatoshis - Satoshi(1)).toMilliSatoshi, open.channelReserveSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } 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 3bc38c87cf..9a88921c3d 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 @@ -17,14 +17,14 @@ package fr.acinq.eclair.channel.states.b import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{MilliSatoshi, TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -41,7 +41,7 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel val setup = init() import setup._ val (fundingSatoshis, pushMsat) = if (test.tags.contains("funder_below_reserve")) { - (1000100L, 1000000000L) // toRemote = 100 satoshis + (Satoshi(1000100L), MilliSatoshi(1000000000L)) // toRemote = 100 satoshis } else { (TestConstants.fundingSatoshis, TestConstants.pushMsat) } @@ -71,13 +71,13 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel test("recv FundingCreated (funder can't pay fees)", Tag("funder_below_reserve")) { f => import f._ - val fees = Transactions.commitWeight * TestConstants.feeratePerKw / 1000 - val reserve = Bob.channelParams.channelReserveSatoshis - val missing = 100 - fees - reserve + val fees = Satoshi(Transactions.commitWeight * TestConstants.feeratePerKw / 1000) + val reserve = Bob.channelParams.channelReserve + val missing = Satoshi(100) - fees - reserve val fundingCreated = alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) val error = bob2alice.expectMsgType[Error] - assert(error === Error(fundingCreated.temporaryChannelId, s"can't pay the fee: missingSatoshis=${-1 * missing} reserveSatoshis=$reserve feesSatoshis=$fees")) + assert(error === Error(fundingCreated.temporaryChannelId, s"can't pay the fee: missing=${-missing} reserve=$reserve fees=$fees")) awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index e327d9cad6..2300d25e05 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -23,6 +23,7 @@ import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} +import fr.acinq.eclair._ import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ @@ -69,7 +70,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val add = CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(50000000), h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -87,7 +88,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = randomBytes32 for (i <- 0 until 10) { - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(50000000), h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == i && htlc.paymentHash == h) @@ -99,8 +100,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, TestConstants.emptyOnionPacket, upstream = Right(originHtlc)) + val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = MilliSatoshi(50000000), cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - MilliSatoshi(10000), h, originHtlc.cltvExpiry - 7, TestConstants.emptyOnionPacket, upstream = Right(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -119,7 +120,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooSmall = currentBlockCount + 3 - val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooSmall, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(500000000), randomBytes32, expiryTooSmall, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooSmall(channelId(alice), currentBlockCount + Channel.MIN_CLTV_EXPIRY, expiryTooSmall, currentBlockCount) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -132,7 +133,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooBig = currentBlockCount + Channel.MAX_CLTV_EXPIRY + 1 - val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooBig, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(500000000), randomBytes32, expiryTooBig, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooBig(channelId(alice), maximum = currentBlockCount + Channel.MAX_CLTV_EXPIRY, actual = expiryTooBig, blockCount = currentBlockCount) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -143,9 +144,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(50, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(50), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = HtlcValueTooSmall(channelId(alice), 1000, 50) + val error = HtlcValueTooSmall(channelId(alice), MilliSatoshi(1000), MilliSatoshi(50)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -154,9 +155,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(Int.MaxValue), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) + val error = InsufficientFunds(channelId(alice), amount = MilliSatoshi(Int.MaxValue), missing = Satoshi(1376443), reserve = Satoshi(20000), fees = Satoshi(8960)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -165,18 +166,18 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(500000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(200000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(67600000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(1000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) + val error = InsufficientFunds(channelId(alice), amount = MilliSatoshi(1000000), missing = Satoshi(1000), reserve = Satoshi(20000), fees = Satoshi(12400)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -185,15 +186,15 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(300000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(300000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(500000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) + val error = InsufficientFunds(channelId(alice), amount = MilliSatoshi(500000000), missing = Satoshi(332400), reserve = Satoshi(20000), fees = Satoshi(12400)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -202,7 +203,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(151000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(bob, add) val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -215,11 +216,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(10000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] } - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(10000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -230,7 +231,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -238,9 +239,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] // this is over channel-capacity - val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add2) - val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) + val error = InsufficientFunds(channelId(alice), add2.amount, Satoshi(564013), Satoshi(20000), Satoshi(10680)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) alice2bob.expectNoMsg(200 millis) } @@ -255,7 +256,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isEmpty) // actual test starts here - val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(500000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -267,14 +268,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // let's make alice send an htlc - val add1 = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(MilliSatoshi(500000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") // at the same time bob initiates a closing sender.send(bob, CMD_CLOSE(None)) sender.expectMsg("ok") // this command will be received by alice right after having received the shutdown - val add2 = CMD_ADD_HTLC(100000000, randomBytes32, 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(MilliSatoshi(100000000), randomBytes32, 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) // messages cross alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -288,7 +289,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(150000), randomBytes32, 400144, TestConstants.emptyOnionPacket) bob ! htlc awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) // bob won't forward the add before it is cross-signed @@ -298,7 +299,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (unexpected id)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, MilliSatoshi(150000), randomBytes32, 400144, TestConstants.emptyOnionPacket) bob ! htlc.copy(id = 0) bob ! htlc.copy(id = 1) bob ! htlc.copy(id = 2) @@ -315,10 +316,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (value too small)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150, randomBytes32, cltvExpiry = 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(150), randomBytes32, cltvExpiry = 400144, TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === HtlcValueTooSmall(channelId(bob), minimum = 1000, actual = 150).getMessage) + assert(new String(error.data.toArray) === HtlcValueTooSmall(channelId(bob), minimum = MilliSatoshi(1000), actual = MilliSatoshi(150)).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) @@ -330,10 +331,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Long.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(Long.MaxValue), randomBytes32, 400144, TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).getMessage) + assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = MilliSatoshi(Long.MaxValue), missing = Satoshi(9223372036083735L), reserve = Satoshi(20000), fees = Satoshi(8960)).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) @@ -345,12 +346,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(400000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(200000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(167600000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, MilliSatoshi(10000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).getMessage) + assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = MilliSatoshi(10000000), missing = Satoshi(11720), reserve = Satoshi(20000), fees = Satoshi(14120)).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) @@ -362,11 +363,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(300000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(300000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(500000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).getMessage) + assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = MilliSatoshi(500000000), missing = Satoshi(332400), reserve = Satoshi(20000), fees = Satoshi(12400)).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) @@ -378,7 +379,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (over max inflight htlc value)") { f => import f._ val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(151000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000).getMessage) awaitCond(alice.stateName == CLOSING) @@ -394,9 +395,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, MilliSatoshi(1000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) } - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(1000000), randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) awaitCond(bob.stateName == CLOSING) @@ -410,7 +411,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") val commitSig = alice2bob.expectMsgType[CommitSig] @@ -421,7 +422,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (two identical htlcs in each direction)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(10000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -453,34 +454,34 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() // for the test to be really useful we have constraint on parameters - assert(Alice.nodeParams.dustLimitSatoshis > Bob.nodeParams.dustLimitSatoshis) + assert(Alice.nodeParams.dustLimit > Bob.nodeParams.dustLimit) // we're gonna exchange two htlcs in each direction, the goal is to have bob's commitment have 4 htlcs, and alice's // commitment only have 3. We will then check that alice indeed persisted 4 htlcs, and bob only 3. - val aliceMinReceive = Alice.nodeParams.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight).toLong - val aliceMinOffer = Alice.nodeParams.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcTimeoutWeight).toLong - val bobMinReceive = Bob.nodeParams.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight).toLong - val bobMinOffer = Bob.nodeParams.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcTimeoutWeight).toLong - val a2b_1 = bobMinReceive + 10 // will be in alice and bob tx - val a2b_2 = bobMinReceive + 20 // will be in alice and bob tx - val b2a_1 = aliceMinReceive + 10 // will be in alice and bob tx - val b2a_2 = bobMinOffer + 10 // will be only be in bob tx + val aliceMinReceive = Alice.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight) + val aliceMinOffer = Alice.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, htlcTimeoutWeight) + val bobMinReceive = Bob.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight) + val bobMinOffer = Bob.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, htlcTimeoutWeight) + val a2b_1 = bobMinReceive + Satoshi(10) // will be in alice and bob tx + val a2b_2 = bobMinReceive + Satoshi(20) // will be in alice and bob tx + val b2a_1 = aliceMinReceive + Satoshi(10) // will be in alice and bob tx + val b2a_2 = bobMinOffer + Satoshi(10) // will be only be in bob tx assert(a2b_1 > aliceMinOffer && a2b_1 > bobMinReceive) assert(a2b_2 > aliceMinOffer && a2b_2 > bobMinReceive) assert(b2a_1 > aliceMinReceive && b2a_1 > bobMinOffer) assert(b2a_2 < aliceMinReceive && b2a_2 > bobMinOffer) - sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_1.toMilliSatoshi, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_2.toMilliSatoshi, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_1.toMilliSatoshi, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) - sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_2.toMilliSatoshi, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) @@ -495,16 +496,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(alice.underlyingActor.nodeParams.db.channels.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 2).size == 4) assert(bob.underlyingActor.nodeParams.db.channels.listHtlcInfos(bob.stateData.asInstanceOf[DATA_NORMAL].channelId, 0).size == 0) assert(bob.underlyingActor.nodeParams.db.channels.listHtlcInfos(bob.stateData.asInstanceOf[DATA_NORMAL].channelId, 1).size == 3) - } + } test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(10000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) val epsilons = List(3, 1, 5, 7, 6) // unordered on purpose val htlcCount = epsilons.size for (i <- epsilons) { - sender.send(alice, add.copy(amountMsat = add.amountMsat + i * 1000)) + sender.send(alice, add.copy(amount = MilliSatoshi(add.amount.toLong + i * 1000))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -532,7 +533,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (while waiting for RevokeAndAck (no pending changes)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -550,7 +551,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (while waiting for RevokeAndAck (with pending changes)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r1, htlc1) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -560,7 +561,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(waitForRevocation.reSignAsap === false) // actual test starts here - val (r2, htlc2) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r2, htlc2) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectNoMsg(300 millis) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation.copy(reSignAsap = true))) @@ -572,7 +573,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // channel starts with all funds on alice's side, so channel will be initially disabled on bob's side assert(Announcements.isEnabled(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags) === false) // alice will send enough funds to bob to make it go above reserve - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) sender.expectMsg("ok") @@ -596,7 +597,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] sender.send(alice, CMD_SIGN) @@ -612,7 +613,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == IN)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.acked.size == 0) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.signed.size == 1) } @@ -621,7 +622,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] sender.send(alice, CMD_SIGN) @@ -637,26 +638,26 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == OUT)) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) } test("recv CommitSig (multiple htlcs in both directions)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r1, htlc1) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r2, htlc2) = addHtlc(MilliSatoshi(8000000), alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + val (r3, htlc3) = addHtlc(MilliSatoshi(300000), bob, alice, bob2alice, alice2bob) // b->a (dust) - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r4, htlc4) = addHtlc(MilliSatoshi(1000000), alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r5, htlc5) = addHtlc(MilliSatoshi(50000000), bob, alice, bob2alice, alice2bob) // b->a (regular) - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + val (r6, htlc6) = addHtlc(MilliSatoshi(500000), alice, bob, alice2bob, bob2alice) // a->b (dust) - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r7, htlc7) = addHtlc(MilliSatoshi(4000000), bob, alice, bob2alice, alice2bob) // b->a (regular) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -698,12 +699,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val r = randomBytes32 val h = Crypto.sha256(r) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(50000000), h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(50000000), h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -714,7 +715,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { crossSign(alice, bob, alice2bob, bob2alice) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc1.id && h.direction == IN)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 2) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.count(_.amount == Satoshi(50000)) == 2) } @@ -737,7 +738,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CommitSig (invalid signature)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // actual test begins @@ -754,7 +755,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx sender.send(alice, CMD_SIGN) @@ -775,7 +776,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx sender.send(alice, CMD_SIGN) @@ -796,7 +797,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (one htlc sent)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -814,7 +815,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (one htlc received)") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -843,19 +844,19 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (multiple htlcs in both directions)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r1, htlc1) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r2, htlc2) = addHtlc(MilliSatoshi(8000000), alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + val (r3, htlc3) = addHtlc(MilliSatoshi(300000), bob, alice, bob2alice, alice2bob) // b->a (dust) - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r4, htlc4) = addHtlc(MilliSatoshi(1000000), alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r5, htlc5) = addHtlc(MilliSatoshi(50000000), bob, alice, bob2alice, alice2bob) // b->a (regular) - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + val (r6, htlc6) = addHtlc(MilliSatoshi(500000), alice, bob, alice2bob, bob2alice) // a->b (dust) - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r7, htlc7) = addHtlc(MilliSatoshi(4000000), bob, alice, bob2alice, alice2bob) // b->a (regular) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -880,13 +881,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (with reSignAsap=true)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r1, htlc1) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) - val (r2, htlc2) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r2, htlc2) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectNoMsg(300 millis) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap === true) @@ -901,7 +902,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -938,7 +939,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (forward UpdateFailHtlc)") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) sender.expectMsg("ok") @@ -967,7 +968,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") @@ -996,7 +997,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevocationTimeout") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -1013,7 +1014,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FULFILL_HTLC") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1040,7 +1041,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FULFILL_HTLC (invalid preimage)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1063,7 +1064,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFulfillHtlc") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) sender.expectMsg("ok") @@ -1083,7 +1084,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1117,7 +1118,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFulfillHtlc (invalid preimage)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) relayerB.expectMsgType[ForwardAdd] val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx @@ -1138,7 +1139,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FAIL_HTLC") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1176,7 +1177,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FAIL_MALFORMED_HTLC") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1221,7 +1222,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFailHtlc") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) sender.expectMsg("ok") @@ -1241,7 +1242,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() // Alice sends an HTLC to Bob, which they both sign - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // Bob fails the HTLC because he cannot parse it val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] @@ -1268,7 +1269,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFailMalformedHtlc (invalid failure_code)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1288,7 +1289,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFailHtlc (sender has not signed htlc)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1396,7 +1397,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) sender.send(bob, fee) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missingSatoshis = 71620000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) + assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = Satoshi(71620000L), reserve = Satoshi(20000L), fees = Satoshi(72400000L)).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) @@ -1446,7 +1447,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_UPDATE_RELAY_FEE ") { f => import f._ val sender = TestProbe() - val newFeeBaseMsat = TestConstants.Alice.nodeParams.feeBaseMsat * 2 + val newFeeBaseMsat = TestConstants.Alice.nodeParams.feeBase * 2 val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.feeProportionalMillionth * 2 sender.send(alice, CMD_UPDATE_RELAY_FEE(newFeeBaseMsat, newFeeProportionalMillionth)) sender.expectMsg("ok") @@ -1471,7 +1472,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_CLOSE (with unacked sent htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_CLOSE(None)) sender.expectMsg(Failure(CannotCloseWithUnsignedOutgoingHtlcs(channelId(bob)))) } @@ -1486,7 +1487,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_CLOSE (with signed sent htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_CLOSE(None)) sender.expectMsg("ok") @@ -1511,7 +1512,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_CLOSE (while waiting for a RevokeAndAck)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1536,7 +1537,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with unacked sent htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_CLOSE(None)) bob2alice.expectMsgType[Shutdown] // actual test begins @@ -1557,7 +1558,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with unacked received htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) // actual test begins sender.send(bob, Shutdown(ByteVector32.Zeroes, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)) bob2alice.expectMsgType[Error] @@ -1581,7 +1582,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with invalid final script and signed htlcs, in response to a Shutdown)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_CLOSE(None)) bob2alice.expectMsgType[Shutdown] @@ -1597,7 +1598,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with signed htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1609,7 +1610,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (while waiting for a RevokeAndAck)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1628,14 +1629,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, CMD_CLOSE(None)) bob2alice.expectMsgType[Shutdown] // this is just so we have something to sign - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) // now we can sign sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // adding an outgoing pending htlc - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r1, htlc1) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) // actual test begins // alice eventually gets bob's shutdown bob2alice.forward(alice) @@ -1665,7 +1666,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (no htlc timed out)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1677,7 +1678,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (an htlc timed out)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1696,7 +1697,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (fulfilled signed htlc ignored by upstream peer)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() @@ -1731,7 +1732,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (fulfilled proposed htlc ignored by upstream peer)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() @@ -1766,7 +1767,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (fulfilled proposed htlc acked but not committed by upstream peer)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() @@ -1844,11 +1845,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob) + val (ra1, htlca1) = addHtlc(MilliSatoshi(250000000), alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(MilliSatoshi(100000000), alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(MilliSatoshi(50000000), bob, alice, bob2alice, alice2bob) + val (rb2, htlcb2) = addHtlc(MilliSatoshi(55000000), bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) @@ -1896,8 +1897,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // assert the feerate of the claim main is what we expect val expectedFeeRate = alice.feeEstimator.getFeeratePerKw(alice.feeTargets.claimMainBlockTarget) - val expectedFee = Transactions.weight2fee(expectedFeeRate, Transactions.claimP2WPKHOutputWeight).toLong - val claimFee = claimMain.txIn.map(in => bobCommitTx.txOut(in.outPoint.index.toInt).amount.toLong).sum - claimMain.txOut.map(_.amount.toLong).sum + val expectedFee = Transactions.weight2fee(expectedFeeRate, Transactions.claimP2WPKHOutputWeight) + val claimFee = claimMain.txIn.map(in => bobCommitTx.txOut(in.outPoint.index.toInt).amount).sum - claimMain.txOut.map(_.amount).sum assert(claimFee == expectedFee) } @@ -1905,11 +1906,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob) + val (ra1, htlca1) = addHtlc(MilliSatoshi(250000000), alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(MilliSatoshi(100000000), alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(MilliSatoshi(50000000), bob, alice, bob2alice, alice2bob) + val (rb2, htlcb2) = addHtlc(MilliSatoshi(55000000), bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) @@ -1971,7 +1972,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // bob = 200 000 def send(): Transaction = { // alice sends 8 000 sat - val (r, htlc) = addHtlc(10000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(10000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx @@ -2030,7 +2031,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // alice = 800 000 // bob = 200 000 - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(10000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -2078,11 +2079,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Error") { f => import f._ - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob) + val (ra1, htlca1) = addHtlc(MilliSatoshi(250000000), alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(MilliSatoshi(100000000), alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(MilliSatoshi(50000000), bob, alice, bob2alice, alice2bob) + val (rb2, htlcb2) = addHtlc(MilliSatoshi(55000000), bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) @@ -2299,8 +2300,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42, null)) val update1a = alice2bob.expectMsgType[ChannelUpdate] assert(Announcements.isEnabled(update1a.channelFlags) == true) - val (_, htlc1) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (_, htlc2) = addHtlc(10000, alice, bob, alice2bob, bob2alice) + val (_, htlc1) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) + val (_, htlc2) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) val aliceData = alice.stateData.asInstanceOf[DATA_NORMAL] assert(aliceData.commitments.localChanges.proposed.size == 2) @@ -2343,8 +2344,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val update1a = channelUpdateListener.expectMsgType[LocalChannelUpdate] val update1b = channelUpdateListener.expectMsgType[LocalChannelUpdate] assert(Announcements.isEnabled(update1a.channelUpdate.channelFlags) == true) - val (_, htlc1) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (_, htlc2) = addHtlc(10000, alice, bob, alice2bob, bob2alice) + val (_, htlc1) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) + val (_, htlc2) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) val aliceData = alice.stateData.asInstanceOf[DATA_NORMAL] assert(aliceData.commitments.localChanges.proposed.size == 2) 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 866a3346f6..916b54c184 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 @@ -31,7 +31,7 @@ import fr.acinq.eclair.payment.CommandBuffer.CommandSend import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32} +import fr.acinq.eclair.{MilliSatoshi, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.Outcome import scala.concurrent.duration._ @@ -66,7 +66,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(1000000), ByteVector32.Zeroes, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob) @@ -143,7 +143,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(1000000), randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob, ab_add_0) @@ -204,11 +204,11 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(MilliSatoshi(250000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(MilliSatoshi(100000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(MilliSatoshi(10000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val oldStateData = alice.stateData fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) @@ -261,7 +261,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // we start by storing the current state val oldStateData = alice.stateData // then we add an htlc and sign it - addHtlc(250000000, alice, bob, alice2bob, bob2alice) + addHtlc(MilliSatoshi(250000000), alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -347,7 +347,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { channelUpdateListener.expectNoMsg(300 millis) // we make alice update here relay fee - sender.send(alice, CMD_UPDATE_RELAY_FEE(4200, 123456)) + sender.send(alice, CMD_UPDATE_RELAY_FEE(MilliSatoshi(4200), 123456)) sender.expectMsg("ok") // alice doesn't broadcast the new channel_update yet @@ -365,7 +365,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // then alice reaches NORMAL state, and after a delay she broadcasts the channel_update val channelUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate](20 seconds).channelUpdate - assert(channelUpdate.feeBaseMsat === 4200) + assert(channelUpdate.feeBaseMsat === MilliSatoshi(4200)) assert(channelUpdate.feeProportionalMillionths === 123456) assert(Announcements.isEnabled(channelUpdate.channelFlags)) @@ -387,7 +387,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { channelUpdateListener.expectNoMsg(300 millis) // we attempt to send a payment - sender.send(alice, CMD_ADD_HTLC(4200, randomBytes32, 123456, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(MilliSatoshi(4200), randomBytes32, 123456, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val failure = sender.expectMsgType[Status.Failure] val AddHtlcFailed(_, _, ChannelUnavailable(_), _, _, _) = failure.cause @@ -401,7 +401,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val register = TestProbe() val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref)) - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() @@ -442,7 +442,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val register = TestProbe() val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref)) - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(alice, INPUT_DISCONNECTED) @@ -452,7 +452,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // We simulate a pending failure on that HTLC. // Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose. - sender.send(commandBuffer, CommandSend(htlc.channelId, htlc.id, CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0))))) + sender.send(commandBuffer, CommandSend(htlc.channelId, htlc.id, CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(MilliSatoshi(0)))))) sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) bob2blockchain.expectNoMsg(250 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 760efbe3f6..0ca1582caa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} +import fr.acinq.eclair.{Globals, MilliSatoshi, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -55,7 +55,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() // alice sends an HTLC to bob val h1 = Crypto.sha256(r1) - val amount1 = 300000000 + val amount1 = MilliSatoshi(300000000) val expiry1 = 400144 val cmd1 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount1, expiry1, h1, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) sender.send(alice, cmd1) @@ -65,7 +65,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.proposed == htlc1 :: Nil) // alice sends another HTLC to bob val h2 = Crypto.sha256(r2) - val amount2 = 200000000 + val amount2 = MilliSatoshi(200000000) val expiry2 = 400144 val cmd2 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount2, expiry2, h2, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) sender.send(alice, cmd2) @@ -104,7 +104,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(500000000), r1, cltvExpiry = 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) @@ -585,7 +585,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) sender.send(bob, fee) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missingSatoshis = 72120000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) + assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = Satoshi(72120000L), reserve = Satoshi(20000L), fees = Satoshi(72400000L)).getMessage) awaitCond(bob.stateName == CLOSING) bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx //bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 3b798fe887..f5c5a59cd4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.payment.Local import fr.acinq.eclair.wire.{ClosingSigned, Error, Shutdown} -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{Globals, MilliSatoshi, TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -85,7 +85,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods import f._ alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(5000000000L), ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) @@ -111,7 +111,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods private def testFeeConverge(f: FixtureParam) = { import f._ - var aliceCloseFee, bobCloseFee = 0L + var aliceCloseFee, bobCloseFee = Satoshi(0) do { aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) @@ -135,9 +135,9 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx - sender.send(bob, aliceCloseSig.copy(feeSatoshis = 99000)) // sig doesn't matter, it is checked later + sender.send(bob, aliceCloseSig.copy(feeSatoshis = Satoshi(99000))) // sig doesn't matter, it is checked later val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=99000")) + assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=Satoshi(99000)")) bob2blockchain.expectMsg(PublishAsap(tx)) bob2blockchain.expectMsgType[PublishAsap] bob2blockchain.expectMsgType[WatchConfirmed] @@ -158,7 +158,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods test("recv BITCOIN_FUNDING_SPENT (counterparty's mutual close)") { f => import f._ - var aliceCloseFee, bobCloseFee = 0L + var aliceCloseFee, bobCloseFee = Satoshi(0) do { aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) @@ -193,7 +193,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods // at this point alice and bob have not yet converged on closing fees, but bob decides to publish a mutual close with one of the previous sigs val d = bob.stateData.asInstanceOf[DATA_NEGOTIATING] implicit val log: LoggingAdapter = bob.underlyingActor.implicitLog - val Success(bobClosingTx) = Closing.checkClosingSignature(Bob.keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(aliceClose1.feeSatoshis), aliceClose1.signature) + val Success(bobClosingTx) = Closing.checkClosingSignature(Bob.keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, aliceClose1.feeSatoshis, aliceClose1.signature) alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx) alice2blockchain.expectMsgType[PublishAsap] 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 b4715a7110..8cff5d868f 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 @@ -22,7 +22,7 @@ import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import com.typesafe.sslconfig.util.NoopLogger -import fr.acinq.bitcoin.{ByteVector32, OutPoint, ScriptFlags, Transaction, TxIn} +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, ScriptFlags, Transaction, TxIn} import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -33,7 +33,7 @@ import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} +import fr.acinq.eclair.{Globals, MilliSatoshi, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -89,7 +89,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { within(30 seconds) { reachNormal(setup) val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000, 200000000, 300000000)) yield { - val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(amt), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) relayerB.expectMsgType[ForwardAdd] val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs @@ -124,7 +124,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice) // agreeing on a closing fee - var aliceCloseFee, bobCloseFee = 0L + var aliceCloseFee, bobCloseFee = Satoshi(0) do { aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) @@ -157,7 +157,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val mutualClosingFeeRate = alice.feeEstimator.getFeeratePerKw(alice.feeTargets.mutualCloseBlockTarget) val expectedFirstProposedFee = Closing.firstClosingFee(aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey, mutualClosingFeeRate)(akka.event.NoLogging) assert(alice.feeTargets.mutualCloseBlockTarget == 2 && mutualClosingFeeRate == 250) - assert(closing.feeSatoshis == expectedFirstProposedFee.amount) + assert(closing.feeSatoshis == expectedFirstProposedFee) } test("recv BITCOIN_FUNDING_PUBLISH_FAILED", Tag("funding_unconfirmed")) { f => @@ -301,7 +301,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test starts here val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(500000000), ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) @@ -380,7 +380,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv BITCOIN_OUTPUT_SPENT") { f => import f._ // alice sends an htlc to bob - val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) relayerB.expectMsgType[ForwardAdd] // an error occurs and alice publishes her commit tx @@ -420,7 +420,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) // alice sends an htlc to bob - val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(MilliSatoshi(50000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx @@ -454,7 +454,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // alice sends an htlc - val (r, htlc) = addHtlc(4200000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(4200000), alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -485,7 +485,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // alice sends an htlc - val (r, htlc) = addHtlc(4200000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(MilliSatoshi(4200000), alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -546,7 +546,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val oldStateData = alice.stateData - val (ra1, htlca1) = addHtlc(25000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(MilliSatoshi(25000000), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) 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 282841c3cc..8892bf3adc 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 @@ -18,14 +18,14 @@ package fr.acinq.eclair.db import java.util.UUID -import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} +import fr.acinq.bitcoin.{Satoshi, Transaction} import fr.acinq.eclair.channel.Channel.{LocalError, RemoteError} import fr.acinq.eclair.channel.{AvailableBalanceChanged, ChannelErrorOccured, NetworkFeePaid} import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} import fr.acinq.eclair.wire.{ChannelCodecs, ChannelCodecsSpec} import fr.acinq.eclair._ +import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} import org.scalatest.FunSuite import concurrent.duration._ @@ -50,8 +50,8 @@ class SqliteAuditDbSpec extends FunSuite { val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(42), "mutual") val e5 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = 0) val e6 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = (Platform.currentTime.milliseconds + 10.minutes).toMillis) - val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000, ChannelCodecsSpec.commitments) - val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000, true, false, "mutual") + val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), MilliSatoshi(456123000), ChannelCodecsSpec.commitments) + val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, Satoshi(456123000), true, false, "mutual") val e9 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), true) val e10 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), true) @@ -97,9 +97,9 @@ class SqliteAuditDbSpec extends FunSuite { db.add(NetworkFeePaid(null, n3, c3, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(400), "funding")) assert(db.stats.toSet === Set( - Stats(channelId = c1, avgPaymentAmountSatoshi = 42, paymentCount = 3, relayFeeSatoshi = 4, networkFeeSatoshi = 100), - Stats(channelId = c2, avgPaymentAmountSatoshi = 40, paymentCount = 1, relayFeeSatoshi = 2, networkFeeSatoshi = 500), - Stats(channelId = c3, avgPaymentAmountSatoshi = 0, paymentCount = 0, relayFeeSatoshi = 0, networkFeeSatoshi = 400) + Stats(channelId = c1, avgPaymentAmount = Satoshi(42), paymentCount = 3, relayFee = Satoshi(4), networkFee = Satoshi(100)), + Stats(channelId = c2, avgPaymentAmount = Satoshi(40), paymentCount = 1, relayFee = Satoshi(2), networkFee = Satoshi(500)), + Stats(channelId = c3, avgPaymentAmount = Satoshi(0), paymentCount = 0, relayFee = Satoshi(0), networkFee = Satoshi(400)) )) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 201cff789a..a721c4d557 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.{Color, NodeAddress, Tor2} -import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.FunSuite import org.sqlite.SQLiteException @@ -85,9 +85,9 @@ class SqliteNetworkDbSpec extends FunSuite { db.removeChannel(channel_2.shortChannelId) assert(db.listChannels().toSet === Set((channel_1, (txid_1, capacity)), (channel_3, (txid_3, capacity)))) - val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(42), 5, 7000000, 50000, 100, 500000000L, true) - val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(43), 5, 7000000, 50000, 100, 500000000L, true) - val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(44), 5, 7000000, 50000, 100, 500000000L, true) + val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(42), 5, MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) + val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(43), 5, MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) + val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(44), 5, MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) assert(db.listChannelUpdates().toSet === Set.empty) db.addChannelUpdate(channel_update_1) @@ -110,7 +110,7 @@ class SqliteNetworkDbSpec extends FunSuite { val capacity = Satoshi(10000) val channels = shortChannelIds.map(id => Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, id, pub, pub, pub, pub, sig, sig, sig, sig)) - val template = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv, pub, ShortChannelId(42), 5, 7000000, 50000, 100, 500000000L, true) + val template = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv, pub, ShortChannelId(42), 5, MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) val updates = shortChannelIds.map(id => template.copy(shortChannelId = id)) val txid = randomBytes32 channels.foreach(ca => db.addChannel(ca, txid, capacity)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index b8eec4b784..86abf94cf0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -17,17 +17,19 @@ package fr.acinq.eclair.db import java.util.UUID + import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.eclair.TestConstants.Bob -import fr.acinq.eclair.{TestConstants, payment} +import fr.acinq.eclair.{MilliSatoshi, TestConstants, payment, randomBytes32} import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import fr.acinq.eclair.payment.PaymentRequest import org.scalatest.FunSuite import scodec.bits._ -import fr.acinq.eclair.randomBytes32 + import scala.compat.Platform import OutgoingPaymentStatus._ + import concurrent.duration._ class SqlitePaymentsDbSpec extends FunSuite { @@ -51,12 +53,12 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 1) == 1) // version 1 is deployed now } - val oldReceivedPayment = IncomingPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 123, 1233322) + val oldReceivedPayment = IncomingPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), MilliSatoshi(123), 1233322) // insert old type record using(connection.prepareStatement("INSERT INTO payments VALUES (?, ?, ?)")) { statement => statement.setBytes(1, oldReceivedPayment.paymentHash.toArray) - statement.setLong(2, oldReceivedPayment.amountMsat) + statement.setLong(2, oldReceivedPayment.amount.toLong) statement.setLong(3, oldReceivedPayment.receivedAt) statement.executeUpdate() } @@ -71,9 +73,9 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(preMigrationDb.getIncomingPayment(oldReceivedPayment.paymentHash).isEmpty) // add a few rows - val ps1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) + val ps1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amount = MilliSatoshi(12345), createdAt = 12345, None, PENDING) val i1 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") - val pr1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) + val pr1 = IncomingPayment(i1.paymentHash, MilliSatoshi(12345678), 1513871928275L) preMigrationDb.addPaymentRequest(i1, ByteVector32.Zeroes) preMigrationDb.addIncomingPayment(pr1) @@ -99,7 +101,7 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(sqlite) // can't receive a payment without an invoice associated with it - assertThrows[IllegalArgumentException](db.addIncomingPayment(IncomingPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad188"), 12345678, 1513871928275L))) + assertThrows[IllegalArgumentException](db.addIncomingPayment(IncomingPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad188"), MilliSatoshi(12345678), 1513871928275L))) val i1 = PaymentRequest.read("lnbc5450n1pw2t4qdpp5vcrf6ylgpettyng4ac3vujsk0zpc25cj0q3zp7l7w44zvxmpzh8qdzz2pshjmt9de6zqen0wgsr2dp4ypcxj7r9d3ejqct5ypekzar0wd5xjuewwpkxzcm99cxqzjccqp2rzjqtspxelp67qc5l56p6999wkatsexzhs826xmupyhk6j8lxl038t27z9tsqqqgpgqqqqqqqlgqqqqqzsqpcz8z8hmy8g3ecunle4n3edn3zg2rly8g4klsk5md736vaqqy3ktxs30ht34rkfkqaffzxmjphvd0637dk2lp6skah2hq09z6lrjna3xqp3d4vyd") val i2 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") @@ -107,8 +109,8 @@ class SqlitePaymentsDbSpec extends FunSuite { db.addPaymentRequest(i1, ByteVector32.Zeroes) db.addPaymentRequest(i2, ByteVector32.Zeroes) - val p1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) - val p2 = IncomingPayment(i2.paymentHash, 12345678, 1513871928275L) + val p1 = IncomingPayment(i1.paymentHash, MilliSatoshi(12345678), 1513871928275L) + val p2 = IncomingPayment(i2.paymentHash, MilliSatoshi(12345678), 1513871928275L) assert(db.listIncomingPayments() === Nil) db.addIncomingPayment(p1) db.addIncomingPayment(p2) @@ -121,8 +123,8 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) - val s1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) - val s2 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) + val s1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amount = MilliSatoshi(12345), createdAt = 12345, None, PENDING) + val s2 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), None, amount = MilliSatoshi(12345), createdAt = 12345, None, PENDING) assert(db.listOutgoingPayments().isEmpty) db.addOutgoingPayment(s1) @@ -135,7 +137,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.getOutgoingPayments(s2.paymentHash) === Seq(s2)) assert(db.getOutgoingPayments(ByteVector32.Zeroes) === Seq.empty) - val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655) + val s3 = s2.copy(id = UUID.randomUUID(), amount = MilliSatoshi(88776655)) db.addOutgoingPayment(s3) db.updateOutgoingPayment(s3.id, FAILED) 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 8b2c2e79da..0f9857d006 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 @@ -24,13 +24,14 @@ import akka.testkit.{TestKit, TestProbe} import com.google.common.net.HostAndPort import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, MilliSatoshi, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed} import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ +import fr.acinq.eclair._ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage} import fr.acinq.eclair.io.{NodeURI, Peer} @@ -65,7 +66,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // we override the default because these test were designed to use cost-optimized routes val integrationTestRouteParams = Some(RouteParams( randomize = false, - maxFeeBaseMsat = Long.MaxValue, + maxFeeBase = MilliSatoshi(Long.MaxValue), maxFeePct = Double.MaxValue, routeMaxCltv = Int.MaxValue, routeMaxLength = ROUTE_MAX_LENGTH, @@ -263,7 +264,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] // then we make the actual payment sender.send(nodes("A").paymentInitiator, - SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1)) + SendPayment(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID](5 seconds) val ps = sender.expectMsgType[PaymentSucceeded](5 seconds) assert(ps.id == paymentId) @@ -279,7 +280,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("B").register, ForwardShortId(shortIdBC, CMD_GETINFO)) val commitmentBC = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].commitments // we then forge a new channel_update for B-C... - val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.expiryDeltaBlocks + 1, nodes("C").nodeParams.htlcMinimumMsat, nodes("B").nodeParams.feeBaseMsat, nodes("B").nodeParams.feeProportionalMillionth, 500000000L) + val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.expiryDeltaBlocks + 1, nodes("C").nodeParams.htlcMinimum, nodes("B").nodeParams.feeBase, nodes("B").nodeParams.feeProportionalMillionth, MilliSatoshi(500000000L)) // ...and notify B's relayer sender.send(nodes("B").relayer, LocalChannelUpdate(system.deadLetters, commitmentBC.channelId, shortIdBC, commitmentBC.remoteParams.nodeId, None, channelUpdateBC, commitmentBC)) // we retrieve a payment hash from D @@ -287,7 +288,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPayment(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment val paymentId = sender.expectMsgType[UUID](5 seconds) @@ -325,7 +326,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // then we make the payment (B-C has a smaller capacity than A-B and C-D) - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPayment(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an error from C, then retry and route around C: A->B->E->C->D sender.expectMsgType[UUID](5 seconds) @@ -334,7 +335,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("send an HTLC A->D with an unknown payment hash") { val sender = TestProbe() - val pr = SendPayment(100000000L, randomBytes32, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val pr = SendPayment(MilliSatoshi(100000000L), randomBytes32, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, pr) // A will receive an error from D and won't retry @@ -343,7 +344,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(MilliSatoshi(100000000L)))) } test("send an HTLC A->D with a lower amount than requested") { @@ -354,7 +355,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] // A send payment of only 1 mBTC - val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPayment(MilliSatoshi(100000000L), pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -363,7 +364,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(MilliSatoshi(100000000L)))) } test("send an HTLC A->D with too much overpayment") { @@ -374,7 +375,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] // A send payment of 6 mBTC - val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPayment(MilliSatoshi(600000000L), pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -383,7 +384,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(paymentId == failed.id) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(MilliSatoshi(600000000L)))) } test("send an HTLC A->D with a reasonable overpayment") { @@ -394,7 +395,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPayment(MilliSatoshi(300000000L), pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -407,7 +408,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 payment")) val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPayment(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSucceeded] // the payment FSM will also reply to the sender after the payment is completed @@ -423,7 +424,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive sender.send(nodes("A").paymentInitiator, - SendPayment(amountMsat.amount, pr.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1)))))) + SendPayment(amountMsat, pr.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1)))))) sender.expectMsgType[UUID](max = 60 seconds) awaitCond({ @@ -467,7 +468,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPayment(MilliSatoshi(100000000L), paymentHash, nodes("F1").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) paymentSender.expectMsgType[UUID](30 seconds) @@ -547,7 +548,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPayment(MilliSatoshi(100000000L), paymentHash, nodes("F2").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) paymentSender.expectMsgType[UUID](30 seconds) @@ -624,7 +625,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage: ByteVector = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPayment(MilliSatoshi(100000000L), paymentHash, nodes("F3").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID] @@ -686,7 +687,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage: ByteVector = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPayment(MilliSatoshi(100000000L), paymentHash, nodes("F4").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID](30 seconds) @@ -760,7 +761,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val amountMsat = MilliSatoshi(300000000L) sender.send(paymentHandlerF, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPayment(300000000L, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1) + val sendReq = SendPayment(MilliSatoshi(300000000L), pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1) sender.send(nodes("A").paymentInitiator, sendReq) val paymentId = sender.expectMsgType[UUID] // we forward the htlc to the payment handler @@ -771,8 +772,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.expectMsgType[PaymentSucceeded].id === paymentId // we now send a few htlcs C->F and F->C in order to obtain a commitments with multiple htlcs - def send(amountMsat: Long, paymentHandler: ActorRef, paymentInitiator: ActorRef) = { - sender.send(paymentHandler, ReceivePayment(Some(MilliSatoshi(amountMsat)), "1 coffee")) + def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef) = { + sender.send(paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] val sendReq = SendPayment(amountMsat, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1) sender.send(paymentInitiator, sendReq) @@ -780,19 +781,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService } val buffer = TestProbe() - send(100000000, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending + send(MilliSatoshi(100000000), paymentHandlerF, nodes("C").paymentInitiator) // will be left pending forwardHandlerF.expectMsgType[UpdateAddHtlc] forwardHandlerF.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] - send(110000000, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending + send(MilliSatoshi(110000000), paymentHandlerF, nodes("C").paymentInitiator) // will be left pending forwardHandlerF.expectMsgType[UpdateAddHtlc] forwardHandlerF.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] - send(120000000, paymentHandlerC, nodes("F5").paymentInitiator) + send(MilliSatoshi(120000000), paymentHandlerC, nodes("F5").paymentInitiator) forwardHandlerC.expectMsgType[UpdateAddHtlc] forwardHandlerC.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] - send(130000000, paymentHandlerC, nodes("F5").paymentInitiator) + send(MilliSatoshi(130000000), paymentHandlerC, nodes("F5").paymentInitiator) forwardHandlerC.expectMsgType[UpdateAddHtlc] forwardHandlerC.forward(buffer.ref) val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments 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 6697116750..9a0fbfde08 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 @@ -21,14 +21,14 @@ import java.util.concurrent.{CountDownLatch, TimeUnit} import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.NoopPaymentHandler import fr.acinq.eclair.wire.Init -import fr.acinq.eclair.{Globals, TestUtils} +import fr.acinq.eclair.{Globals, MilliSatoshi, TestUtils} import org.scalatest.{BeforeAndAfterAll, Matchers, Outcome, fixture} import scala.concurrent.duration._ @@ -60,7 +60,7 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(10000)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000, 1000000000, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, Satoshi(2000000), MilliSatoshi(1000000000), feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) within(30 seconds) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index 2c72623576..1a2f5eabe6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -22,7 +22,7 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.{TestConstants, TestUtils} +import fr.acinq.eclair.{MilliSatoshi, TestConstants, TestUtils} import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.{IN, OUT} @@ -57,7 +57,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging script match { case offer(x, amount, rhash) :: rest => - resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + resolve(x) ! CMD_ADD_HTLC(MilliSatoshi(amount.toInt), ByteVector32.fromValidHex(rhash), 144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) exec(rest, a, b) case fulfill(x, id, r) :: rest => resolve(x) ! CMD_FULFILL_HTLC(id.toInt, ByteVector32.fromValidHex(r)) @@ -134,15 +134,15 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging s" Commit ${d.commitments.localCommit.index}:", s" Offered htlcs: ${localCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", s" Received htlcs: ${localCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", - s" Balance us: ${localCommit.spec.toLocalMsat}", - s" Balance them: ${localCommit.spec.toRemoteMsat}", + s" Balance us: ${localCommit.spec.toLocal}", + s" Balance them: ${localCommit.spec.toRemote}", s" Fee rate: ${localCommit.spec.feeratePerKw}", "REMOTE COMMITS:", s" Commit ${remoteCommit.index}:", s" Offered htlcs: ${remoteCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", s" Received htlcs: ${remoteCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", - s" Balance us: ${remoteCommit.spec.toLocalMsat}", - s" Balance them: ${remoteCommit.spec.toRemoteMsat}", + s" Balance us: ${remoteCommit.spec.toLocal}", + s" Balance them: ${remoteCommit.spec.toRemote}", s" Fee rate: ${remoteCommit.spec.feeratePerKw}") .foreach(s => { fout.write(rtrim(s)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala index 4378f5cf1d..57debf6a86 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.io import akka.actor.{ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.channel._ import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.{ChannelCodecsSpec, TemporaryNodeFailure, UpdateAddHtlc} @@ -36,11 +37,11 @@ class HtlcReaperSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val data = ChannelCodecsSpec.normal // assuming that data has incoming htlcs 0 and 1, we don't care about the amount/payment_hash/onion fields - val add0 = UpdateAddHtlc(data.channelId, 0, 20000, randomBytes32, 100, TestConstants.emptyOnionPacket) - val add1 = UpdateAddHtlc(data.channelId, 1, 30000, randomBytes32, 100, TestConstants.emptyOnionPacket) + val add0 = UpdateAddHtlc(data.channelId, 0, MilliSatoshi(20000), randomBytes32, 100, TestConstants.emptyOnionPacket) + val add1 = UpdateAddHtlc(data.channelId, 1, MilliSatoshi(30000), randomBytes32, 100, TestConstants.emptyOnionPacket) // unrelated htlc - val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678, randomBytes32, 100, TestConstants.emptyOnionPacket) + val add99 = UpdateAddHtlc(randomBytes32, 0, MilliSatoshi(12345678), randomBytes32, 100, TestConstants.emptyOnionPacket) val brokenHtlcs = Seq(add0, add1, add99) val brokenHtlcKiller = system.actorOf(Props[HtlcReaper], name = "htlc-reaper") 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 246f9bc202..074169d83c 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,7 @@ 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.{MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{Satoshi} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index ccc65efe0b..71027c5e10 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC} import fr.acinq.eclair.payment.Relayer.{OutgoingChannel, RelayFailure, RelayPayload, RelaySuccess} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TestConstants, randomBytes32, randomKey} import fr.acinq.eclair.payment.HtlcGenerationSpec.makeCommitments import org.scalatest.FunSuite @@ -33,17 +33,17 @@ class ChannelSelectionSpec extends FunSuite { /** * This is just a simplified helper function with random values for fields we are not using here */ - def dummyUpdate(shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Long, enable: Boolean = true) = - Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, shortChannelId, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, htlcMaximumMsat, enable) + def dummyUpdate(shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Long, enable: Boolean = true) = + Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, shortChannelId, cltvExpiryDelta, htlcMinimumMsat, MilliSatoshi(feeBaseMsat), feeProportionalMillionths, MilliSatoshi(htlcMaximumMsat), enable) test("convert to CMD_FAIL_HTLC/CMD_ADD_HTLC") { val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), - payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), + add = UpdateAddHtlc(randomBytes32, 42, MilliSatoshi(1000000), randomBytes32, 70, TestConstants.emptyOnionPacket), + payload = PerHopPayload(ShortChannelId(12345), amtToForward = MilliSatoshi(998900), outgoingCltvValue = 60), nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) - val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) + val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, MilliSatoshi(100), 1000, 100, 10000000, true) implicit val log = akka.event.NoLogging @@ -55,36 +55,36 @@ class ChannelSelectionSpec extends FunSuite { val channelUpdate_disabled = channelUpdate.copy(channelFlags = Announcements.makeChannelFlags(true, enable = false)) assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate_disabled)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(ChannelDisabled(channelUpdate_disabled.messageFlags, channelUpdate_disabled.channelFlags, channelUpdate_disabled)), commit = true))) // amount too low - val relayPayload_toolow = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 99)) + val relayPayload_toolow = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = MilliSatoshi(99))) assert(Relayer.relayOrFail(relayPayload_toolow, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(AmountBelowMinimum(relayPayload_toolow.payload.amtToForward, channelUpdate)), commit = true))) // incorrect cltv expiry val relayPayload_incorrectcltv = relayPayload.copy(payload = relayPayload.payload.copy(outgoingCltvValue = 42)) assert(Relayer.relayOrFail(relayPayload_incorrectcltv, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(IncorrectCltvExpiry(relayPayload_incorrectcltv.payload.outgoingCltvValue, channelUpdate)), commit = true))) // insufficient fee - val relayPayload_insufficientfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 998910)) + val relayPayload_insufficientfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = MilliSatoshi(998910))) assert(Relayer.relayOrFail(relayPayload_insufficientfee, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(FeeInsufficient(relayPayload_insufficientfee.add.amountMsat, channelUpdate)), commit = true))) // note that a generous fee is ok! - val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 900000)) + val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = MilliSatoshi(900000))) assert(Relayer.relayOrFail(relayPayload_highfee, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket, upstream = Right(relayPayload.add), commit = true))) } test("channel selection") { val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), - payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), + add = UpdateAddHtlc(randomBytes32, 42, MilliSatoshi(1000000), randomBytes32, 70, TestConstants.emptyOnionPacket), + payload = PerHopPayload(ShortChannelId(12345), amtToForward = MilliSatoshi(998900), outgoingCltvValue = 60), nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) val (a, b) = (randomKey.publicKey, randomKey.publicKey) - val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) + val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, MilliSatoshi(100), 1000, 100, 10000000, true) val channelUpdates = Map( - ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000000)), - ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 20000000)), - ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 10000000)), - ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000)), - ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, makeCommitments(ByteVector32.Zeroes, 1000000)) + ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, MilliSatoshi(100000000))), + ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, MilliSatoshi(20000000))), + ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, MilliSatoshi(10000000))), + ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, MilliSatoshi(100000))), + ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, makeCommitments(ByteVector32.Zeroes, MilliSatoshi(1000000))) ) val node2channels = new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId] @@ -104,11 +104,11 @@ class ChannelSelectionSpec extends FunSuite { // all the suitable channels have been tried assert(Relayer.selectPreferredChannel(relayPayload, channelUpdates, node2channels, Seq(ShortChannelId(22222), ShortChannelId(12345), ShortChannelId(11111))) === None) // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) - assert(Relayer.selectPreferredChannel(relayPayload.modify(_.add.amountMsat).setTo(60000000).modify(_.payload.amtToForward).setTo(50000000), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(11111))) + assert(Relayer.selectPreferredChannel(relayPayload.modify(_.add.amountMsat).setTo(MilliSatoshi(60000000)).modify(_.payload.amtToForward).setTo(MilliSatoshi(50000000)), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(11111))) // lower amount payment - assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.amtToForward).setTo(1000), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(33333))) + assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.amtToForward).setTo(MilliSatoshi(1000)), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(33333))) // payment too high, no suitable channel found - assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.amtToForward).setTo(1000000000), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(12345))) + assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.amtToForward).setTo(MilliSatoshi(1000000000)), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(12345))) // invalid cltv expiry, no suitable channel, we keep the requested one assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.outgoingCltvValue).setTo(40), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(12345))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index 9114a8e418..02252bc923 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -20,13 +20,14 @@ import java.util.UUID import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet} +import fr.acinq.eclair.maxOf import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.{DecryptedPacket, PacketAndSecrets} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire.{ChannelUpdate, OnionCodecs, PerHopPayload} -import fr.acinq.eclair.{ShortChannelId, TestConstants, nodeFee, randomBytes32} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TestConstants, nodeFee, randomBytes32} import org.scalatest.FunSuite import scodec.bits.ByteVector @@ -37,9 +38,9 @@ import scodec.bits.ByteVector class HtlcGenerationSpec extends FunSuite { test("compute fees") { - val feeBaseMsat = 150000L + val feeBaseMsat = MilliSatoshi(150000L) val feeProportionalMillionth = 4L - val htlcAmountMsat = 42000000 + val htlcAmountMsat = MilliSatoshi(42000000) // spec: fee-base-msat + htlc-amount-msat * fee-proportional-millionths / 1000000 val ref = feeBaseMsat + htlcAmountMsat * feeProportionalMillionth / 1000000 val fee = nodeFee(feeBaseMsat, feeProportionalMillionth, htlcAmountMsat) @@ -98,7 +99,7 @@ class HtlcGenerationSpec extends FunSuite { val (add, _) = buildCommand(UUID.randomUUID, finalAmountMsat, finalExpiry, paymentHash, hops) - assert(add.amountMsat > finalAmountMsat) + assert(add.amount > finalAmountMsat) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash === paymentHash) assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) @@ -132,7 +133,7 @@ class HtlcGenerationSpec extends FunSuite { test("build a command with no hops") { val (add, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops.take(1)) - assert(add.amountMsat === finalAmountMsat) + assert(add.amount === finalAmountMsat) assert(add.cltvExpiry === finalExpiry) assert(add.paymentHash === paymentHash) assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) @@ -149,10 +150,10 @@ class HtlcGenerationSpec extends FunSuite { object HtlcGenerationSpec { - def makeCommitments(channelId: ByteVector32, availableBalanceForSend: Long = 50000000L, availableBalanceForReceive: Long = 50000000L) = + def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = MilliSatoshi(50000000L), testAvailableBalanceForReceive: MilliSatoshi = MilliSatoshi(50000000L)) = new Commitments(ChannelVersion.STANDARD, null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId) { - override lazy val availableBalanceForSendMsat: Long = availableBalanceForSend.max(0) - override lazy val availableBalanceForReceiveMsat: Long = availableBalanceForReceive.max(0) + override lazy val availableBalanceForSend: MilliSatoshi = maxOf(testAvailableBalanceForSend, MilliSatoshi(0)) + override lazy val availableBalanceForReceive: MilliSatoshi = maxOf(testAvailableBalanceForReceive, MilliSatoshi(0)) } def randomExtendedPrivateKey: ExtendedPrivateKey = DeterministicWallet.generate(randomBytes32) @@ -160,11 +161,11 @@ object HtlcGenerationSpec { val (priv_a, priv_b, priv_c, priv_d, priv_e) = (TestConstants.Alice.keyManager.nodeKey, TestConstants.Bob.keyManager.nodeKey, randomExtendedPrivateKey, randomExtendedPrivateKey, randomExtendedPrivateKey) val (a, b, c, d, e) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey) val sig = Crypto.sign(Crypto.sha256(ByteVector.empty), priv_a.privateKey) - val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, 1, 0, 0, 42000, 0, 0, Some(500000000L)) - val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(1), cltvExpiryDelta = 4, feeBaseMsat = 642000, feeProportionalMillionths = 7) - val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(2), cltvExpiryDelta = 5, feeBaseMsat = 153000, feeProportionalMillionths = 4) - val channelUpdate_cd = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(3), cltvExpiryDelta = 10, feeBaseMsat = 60000, feeProportionalMillionths = 1) - val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(4), cltvExpiryDelta = 7, feeBaseMsat = 766000, feeProportionalMillionths = 10) + val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, 1, 0, 0, MilliSatoshi(42000), MilliSatoshi(0), 0, Some(MilliSatoshi(500000000L))) + val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(1), cltvExpiryDelta = 4, feeBaseMsat = MilliSatoshi(642000), feeProportionalMillionths = 7) + val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(2), cltvExpiryDelta = 5, feeBaseMsat = MilliSatoshi(153000), feeProportionalMillionths = 4) + val channelUpdate_cd = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(3), cltvExpiryDelta = 10, feeBaseMsat = MilliSatoshi(60000), feeProportionalMillionths = 1) + val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(4), cltvExpiryDelta = 7, feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10) // simple route a -> b -> c -> d -> e @@ -174,7 +175,7 @@ object HtlcGenerationSpec { Hop(c, d, channelUpdate_cd) :: Hop(d, e, channelUpdate_de) :: Nil - val finalAmountMsat = 42000000L + val finalAmountMsat = MilliSatoshi(42000000L) val currentBlockCount = 420000 val finalExpiry = currentBlockCount + Channel.MIN_CLTV_EXPIRY val paymentPreimage = randomBytes32 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 3915d24895..4a1b21f452 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -19,13 +19,13 @@ package fr.acinq.eclair.payment import akka.actor.Status.Failure import akka.actor.ActorSystem import akka.testkit.{TestActorRef, TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} -import fr.acinq.eclair.{Globals, ShortChannelId, TestConstants, randomKey} +import fr.acinq.eclair.{Globals, MilliSatoshi, ShortChannelId, TestConstants, randomKey} import org.scalatest.FunSuiteLike import scodec.bits.ByteVector @@ -54,7 +54,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike assert(nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).isDefined) assert(!nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).get._2.isExpired) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] @@ -68,7 +68,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] @@ -81,7 +81,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, TestConstants.emptyOnionPacket) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) @@ -164,7 +164,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, ReceivePayment(Some(amountMsat), "some desc", expirySeconds_opt = Some(0))) val pr = sender.expectMsgType[PaymentRequest] - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FAIL_HTLC] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 79192309bf..9d178c0819 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -22,7 +22,7 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, Transaction, TxOut} import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic} import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} @@ -42,7 +42,7 @@ import fr.acinq.eclair._ class PaymentLifecycleSpec extends BaseRouterSpec { - val defaultAmountMsat = 142000000L + val defaultAmountMsat = MilliSatoshi(142000000L) test("send to route") { fixture => import fixture._ @@ -109,7 +109,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, randomBytes32, d, routeParams = Some(RouteParams(randomize = false, maxFeeBaseMsat = 100, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = 2016, ratios = None)), maxAttempts = 5) + val request = SendPayment(defaultAmountMsat, randomBytes32, d, routeParams = Some(RouteParams(randomize = false, maxFeeBase = MilliSatoshi(100), maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = 2016, ratios = None)), maxAttempts = 5) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -389,9 +389,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] - val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, request.amount, fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) - assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) + assert(fee === paymentOK.amount - request.amount) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) } @@ -409,8 +409,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil) val channelId_bg = ShortChannelId(420000, 5, 0) val chan_bg = channelAnnouncement(channelId_bg, priv_b, priv_g, priv_funding_b, priv_funding_g) - val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 0, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000L) - val channelUpdate_gb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, b, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L) + val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(0), feeProportionalMillionths = 0, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_gb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, b, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 8, htlcMaximumMsat = MilliSatoshi(500000000L)) assert(Router.getDesc(channelUpdate_bg, chan_bg) === ChannelDesc(chan_bg.shortChannelId, priv_b.publicKey, priv_g.publicKey)) router ! PeerRoutingMessage(null, remoteNodeId, chan_bg) router ! PeerRoutingMessage(null, remoteNodeId, ann_g) @@ -441,13 +441,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] - val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, request.amount, fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] // during the route computation the fees were treated as if they were 1msat but when sending the onion we actually put zero // NB: A -> B doesn't pay fees because it's our direct neighbor // NB: B -> G doesn't asks for fees at all assert(fee === MilliSatoshi(0)) - assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) + assert(fee === paymentOK.amount - request.amount) } test("filter errors properly") { _ => 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 f67bd0e074..19706bcac9 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 @@ -17,14 +17,15 @@ package fr.acinq.eclair.payment import java.nio.ByteOrder - import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi} -import fr.acinq.eclair.ShortChannelId +import fr.acinq.bitcoin.{Block, Btc, ByteVector32, Crypto, MilliBtc, Protocol, Satoshi} +import fr.acinq.bitcoin._ +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.payment.PaymentRequest._ import org.scalatest.FunSuite import scodec.DecodeResult import scodec.bits._ +import fr.acinq.eclair._ /** * Created by fabrice on 15/05/17. @@ -42,13 +43,13 @@ class PaymentRequestSpec extends FunSuite { assert('p' === Amount.unit(MilliSatoshi(99))) assert('n' === Amount.unit(MilliSatoshi(100))) assert('p' === Amount.unit(MilliSatoshi(101))) - assert('n' === Amount.unit(Satoshi(1))) - assert('u' === Amount.unit(Satoshi(100))) - assert('n' === Amount.unit(Satoshi(101))) - assert('u' === Amount.unit(Satoshi(1155400))) - assert('m' === Amount.unit(MilliBtc(1))) - assert('m' === Amount.unit(MilliBtc(10))) - assert('m' === Amount.unit(Btc(1))) + assert('n' === Amount.unit(Satoshi(1).toMilliSatoshi)) + assert('u' === Amount.unit(Satoshi(100).toMilliSatoshi)) + assert('n' === Amount.unit(Satoshi(101).toMilliSatoshi)) + assert('u' === Amount.unit(Satoshi(1155400).toMilliSatoshi)) + assert('m' === Amount.unit(millibtc2satoshi(MilliBtc(1)).toMilliSatoshi)) + assert('m' === Amount.unit(millibtc2satoshi(MilliBtc(10)).toMilliSatoshi)) + assert('m' === Amount.unit(btc2satoshi(Btc(1)).toMilliSatoshi)) } test("check that we can still decode non-minimal amount encoding") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index c6f6a27d41..97db737ee4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -20,13 +20,13 @@ import java.util.UUID import akka.actor.{ActorRef, Status} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, TestConstants, TestkitBaseClass, UInt64, randomBytes32} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TestConstants, TestkitBaseClass, UInt64, randomBytes32} import org.scalatest.Outcome import scodec.bits.ByteVector @@ -63,7 +63,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -83,14 +83,14 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) // we tell the relayer about channel B-C relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) // this is another channel B-C, with less balance (it will be preferred) val (channelId_bc_1, channelUpdate_bc_1) = (randomBytes32, channelUpdate_bc.copy(shortChannelId = ShortChannelId("500000x1x1"))) - relayer ! LocalChannelUpdate(null, channelId_bc_1, channelUpdate_bc_1.shortChannelId, c, None, channelUpdate_bc_1, makeCommitments(channelId_bc_1, 49000000L)) + relayer ! LocalChannelUpdate(null, channelId_bc_1, channelUpdate_bc_1.shortChannelId, c, None, channelUpdate_bc_1, makeCommitments(channelId_bc_1, MilliSatoshi(49000000L))) sender.send(relayer, ForwardAdd(add_ab)) @@ -100,7 +100,7 @@ class RelayerSpec extends TestkitBaseClass { assert(fwd1.message.upstream === Right(add_ab)) // channel returns an error - val origin = Relayed(channelId_ab, originHtlcId = 42, amountMsatIn = 1100000, amountMsatOut = 1000000) + val origin = Relayed(channelId_ab, originHtlcId = 42, amountIn = MilliSatoshi(1100000), amountOut = MilliSatoshi(1000000)) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc_1, paymentHash, HtlcValueTooHighInFlight(channelId_bc_1, UInt64(1000000000L), UInt64(1516977616L)), origin, Some(channelUpdate_bc_1), originalCommand = Some(fwd1.message)))) // second try @@ -128,7 +128,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) sender.send(relayer, ForwardAdd(add_ab)) @@ -148,7 +148,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -174,7 +174,7 @@ class RelayerSpec extends TestkitBaseClass { // check that payments are sent properly val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -190,7 +190,7 @@ class RelayerSpec extends TestkitBaseClass { relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId) val (cmd1, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, randomBytes32, hops) - val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) + val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amount, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) sender.send(relayer, ForwardAdd(add_ab1)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message @@ -208,7 +208,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = Announcements.makeChannelFlags(Announcements.isNode1(channelUpdate_bc.channelFlags), enable = false)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc_disabled, makeCommitments(channelId_bc)) @@ -229,7 +229,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc with an invalid onion (hmac) - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -248,16 +248,16 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0)))) + val (cmd, _) = buildCommand(UUID.randomUUID(), channelUpdate_bc.htlcMinimumMsat - MilliSatoshi(1), finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = MilliSatoshi(0), feeProportionalMillionths = 0)))) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(fail.id === add_ab.id) - assert(fail.reason == Right(AmountBelowMinimum(cmd.amountMsat, channelUpdate_bc))) + assert(fail.reason == Right(AmountBelowMinimum(cmd.amount, channelUpdate_bc))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -270,7 +270,7 @@ class RelayerSpec extends TestkitBaseClass { val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0))) val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -290,7 +290,7 @@ class RelayerSpec extends TestkitBaseClass { val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(feeBaseMsat = hops(1).lastUpdate.feeBaseMsat / 2))) val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -311,7 +311,7 @@ class RelayerSpec extends TestkitBaseClass { val hops1 = hops.head :: Nil val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc with a wrong expiry - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 1, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount - MilliSatoshi(1), cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -332,7 +332,7 @@ class RelayerSpec extends TestkitBaseClass { val hops1 = hops.head :: Nil val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc with a wrong expiry - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry - 1, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry - 1, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -350,7 +350,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() val paymentHash = randomBytes32 - val origin = Relayed(channelId_ab, originHtlcId = 42, amountMsatIn = 1100000, amountMsatOut = 1000000) + val origin = Relayed(channelId_ab, originHtlcId = 42, amountIn = MilliSatoshi(1100000), amountOut = MilliSatoshi(1000000)) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ExpiryTooSmall(channelId_bc, 100, 0, 0), origin, Some(channelUpdate_bc), None))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ExpiryTooSoon(channelUpdate_bc))) @@ -358,7 +358,7 @@ class RelayerSpec extends TestkitBaseClass { sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ExpiryTooBig(channelId_bc, 100, 200, 0), origin, Some(channelUpdate_bc), None))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ExpiryTooFar)) - sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, InsufficientFunds(channelId_bc, origin.amountMsatOut, 100, 0, 0), origin, Some(channelUpdate_bc), None))) + sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, InsufficientFunds(channelId_bc, origin.amountOut, Satoshi(100), Satoshi(0), Satoshi(0)), origin, Some(channelUpdate_bc), None))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(TemporaryChannelFailure(channelUpdate_bc))) val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = 2) @@ -383,9 +383,9 @@ class RelayerSpec extends TestkitBaseClass { system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = MilliSatoshi(10000000L), paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 42, paymentPreimage = ByteVector32.Zeroes) - val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) + val origin = Relayed(channelId_ab, 150, MilliSatoshi(11000000L), MilliSatoshi(10000000L)) sender.send(relayer, ForwardFulfill(fulfill_ba, origin, add_bc)) val fwd = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] @@ -393,7 +393,7 @@ class RelayerSpec extends TestkitBaseClass { assert(fwd.message.id === origin.originHtlcId) val paymentRelayed = eventListener.expectMsgType[PaymentRelayed] - assert(paymentRelayed.copy(timestamp = 0) === PaymentRelayed(MilliSatoshi(origin.amountMsatIn), MilliSatoshi(origin.amountMsatOut), add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0)) + assert(paymentRelayed.copy(timestamp = 0) === PaymentRelayed(origin.amountIn, origin.amountOut, add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0)) } test("relay an htlc-fail") { f => @@ -401,9 +401,9 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = MilliSatoshi(10000000L), paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.FailurePacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) - val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) + val origin = Relayed(channelId_ab, 150, MilliSatoshi(11000000L), MilliSatoshi(10000000L)) sender.send(relayer, ForwardFail(fail_ba, origin, add_bc)) val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] @@ -414,31 +414,31 @@ class RelayerSpec extends TestkitBaseClass { test("get usable balances") { f => import f._ val sender = TestProbe() - relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000, 300000)) - relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000, -5000)) + relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, MilliSatoshi(-2000), MilliSatoshi(300000))) + relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, MilliSatoshi(400000), MilliSatoshi(-5000))) sender.send(relayer, GetUsableBalances) val usableBalances1 = sender.expectMsgType[Iterable[UsableBalances]] assert(usableBalances1.size === 2) - assert(usableBalances1.head.canSendMsat === 0 && usableBalances1.head.canReceiveMsat === 300000 && usableBalances1.head.shortChannelId == channelUpdate_ab.shortChannelId) - assert(usableBalances1.last.canReceiveMsat === 0 && usableBalances1.last.canSendMsat === 400000 && usableBalances1.last.shortChannelId == channelUpdate_bc.shortChannelId) + assert(usableBalances1.head.canSend === MilliSatoshi(0) && usableBalances1.head.canReceive === MilliSatoshi(300000) && usableBalances1.head.shortChannelId == channelUpdate_ab.shortChannelId) + assert(usableBalances1.last.canReceive === MilliSatoshi(0) && usableBalances1.last.canSend === MilliSatoshi(400000) && usableBalances1.last.shortChannelId == channelUpdate_bc.shortChannelId) - relayer ! AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, 0, makeCommitments(channelId_bc, 200000, 500000)) + relayer ! AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, MilliSatoshi(0), makeCommitments(channelId_bc, MilliSatoshi(200000), MilliSatoshi(500000))) sender.send(relayer, GetUsableBalances) val usableBalances2 = sender.expectMsgType[Iterable[UsableBalances]] - assert(usableBalances2.last.canReceiveMsat === 500000 && usableBalances2.last.canSendMsat === 200000) + assert(usableBalances2.last.canReceive === MilliSatoshi(500000) && usableBalances2.last.canSend === MilliSatoshi(200000)) - relayer ! AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, 0, makeCommitments(channelId_ab, 100000, 200000)) + relayer ! AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, MilliSatoshi(0), makeCommitments(channelId_ab, MilliSatoshi(100000), MilliSatoshi(200000))) relayer ! LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c) sender.send(relayer, GetUsableBalances) val usableBalances3 = sender.expectMsgType[Iterable[UsableBalances]] - assert(usableBalances3.size === 1 && usableBalances3.head.canSendMsat === 100000) + assert(usableBalances3.size === 1 && usableBalances3.head.canSend === MilliSatoshi(100000)) - relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = 2), makeCommitments(channelId_ab, 100000, 200000)) + relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = 2), makeCommitments(channelId_ab, MilliSatoshi(100000), MilliSatoshi(200000))) sender.send(relayer, GetUsableBalances) val usableBalances4 = sender.expectMsgType[Iterable[UsableBalances]] assert(usableBalances4.isEmpty) - relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000, 200000)) + relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, MilliSatoshi(100000), MilliSatoshi(200000))) sender.send(relayer, GetUsableBalances) val usableBalances5 = sender.expectMsgType[Iterable[UsableBalances]] assert(usableBalances5.size === 1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index 0667d2a7bd..a3c29fda8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -27,10 +27,9 @@ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{ShortChannelId, randomKey} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, randomKey} import org.scalatest.FunSuite import scodec.bits.ByteVector - import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext} @@ -104,6 +103,6 @@ object AnnouncementsBatchValidationSpec { } def makeChannelUpdate(c: SimulatedChannel, shortChannelId: ShortChannelId): ChannelUpdate = - Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, c.node1Key, c.node2Key.publicKey, shortChannelId, 10, 1000, 10, 100, 500000000L) + Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, c.node1Key, c.node2Key.publicKey, shortChannelId, 10, MilliSatoshi(1000), MilliSatoshi(10), 100, MilliSatoshi(500000000L)) } \ No newline at end of file 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 d64bfab29f..41b5c722f1 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.{Block} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair._ @@ -55,7 +55,7 @@ class AnnouncementsSpec extends FunSuite { } test("create valid signed channel update announcement") { - val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey.publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth, 500000000L) + val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey.publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimum, Alice.nodeParams.feeBase, Alice.nodeParams.feeProportionalMillionth, MilliSatoshi(500000000L)) assert(checkSig(ann, Alice.nodeParams.nodeId)) assert(checkSig(ann, randomKey.publicKey) === false) } @@ -66,10 +66,10 @@ class AnnouncementsSpec extends FunSuite { // NB: node1 < node2 (public keys) assert(isNode1(node1_priv.publicKey, node2_priv.publicKey)) assert(!isNode1(node2_priv.publicKey, node1_priv.publicKey)) - val channelUpdate1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = true) - val channelUpdate1_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = false) - val channelUpdate2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = true) - val channelUpdate2_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = false) + val channelUpdate1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, MilliSatoshi(0), MilliSatoshi(0), 0, MilliSatoshi(500000000L), enable = true) + val channelUpdate1_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, MilliSatoshi(0), MilliSatoshi(0), 0, MilliSatoshi(500000000L), enable = false) + val channelUpdate2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, MilliSatoshi(0), MilliSatoshi(0), 0, MilliSatoshi(500000000L), enable = true) + val channelUpdate2_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, MilliSatoshi(0), MilliSatoshi(0), 0, MilliSatoshi(500000000L), enable = false) assert(channelUpdate1.channelFlags == 0) // ....00 assert(channelUpdate1_disabled.channelFlags == 2) // ....10 assert(channelUpdate2.channelFlags == 1) // ....01 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index fc63698bb1..4acdc1db69 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -78,14 +78,14 @@ abstract class BaseRouterSpec extends TestkitBaseClass { val chan_cd = channelAnnouncement(channelId_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) val chan_ef = channelAnnouncement(channelId_ef, priv_e, priv_f, priv_funding_e, priv_funding_f) - val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L) - val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L) - val channelUpdate_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000L) - val channelUpdate_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000L) - val channelUpdate_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L) - val channelUpdate_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L) - val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L) - val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L) + val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 10, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 10, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 1, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 1, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 4, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 4, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 8, htlcMaximumMsat = MilliSatoshi(500000000L)) + val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(10), feeProportionalMillionths = 8, htlcMaximumMsat = MilliSatoshi(500000000L)) override def withFixture(test: OneArgTest): Outcome = { // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 9e0506aa5d..9962ba50dd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.router -import fr.acinq.eclair.randomKey +import fr.acinq.eclair.{MilliSatoshi, randomKey} import fr.acinq.eclair.wire._ import org.scalatest.FunSuite @@ -33,14 +33,14 @@ class ChannelRangeQueriesSpec extends FunSuite { val a = randomKey.publicKey val b = randomKey.publicKey val ab = RouteCalculationSpec.makeChannel(123466L, a, b) - val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, 0, 0, timestamp = now) - val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, 0, 0, timestamp = now) + val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, MilliSatoshi(0), 0, timestamp = now) + val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, MilliSatoshi(0), 0, timestamp = now) val c = randomKey.publicKey val d = randomKey.publicKey val cd = RouteCalculationSpec.makeChannel(451312L, c, d) - val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, 0, 0, timestamp = now) - val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, 0, 0, timestamp = now) + val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, MilliSatoshi(0), 0, timestamp = now) + val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, MilliSatoshi(0), 0, timestamp = now) val e = randomKey.publicKey val f = randomKey.publicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index 85c0e2ce82..9730365850 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.RouteCalculationSpec._ import fr.acinq.eclair.wire.ChannelUpdate @@ -46,12 +46,12 @@ class GraphSpec extends FunSuite { def makeTestGraph() = { val updates = Seq( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, a, d, 0, 0), - makeUpdate(4L, d, c, 0, 0), - makeUpdate(5L, c, e, 0, 0), - makeUpdate(6L, b, e, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, a, d, MilliSatoshi(0), 0), + makeUpdate(4L, d, c, MilliSatoshi(0), 0), + makeUpdate(5L, c, e, MilliSatoshi(0), 0), + makeUpdate(6L, b, e, MilliSatoshi(0), 0) ) DirectedGraph.makeGraph(updates.toMap) @@ -72,11 +72,11 @@ class GraphSpec extends FunSuite { assert(otherGraph.vertexSet().size === 5) // add some edges to the graph - val (descAB, updateAB) = makeUpdate(1L, a, b, 0, 0) - val (descBC, updateBC) = makeUpdate(2L, b, c, 0, 0) - val (descAD, updateAD) = makeUpdate(3L, a, d, 0, 0) - val (descDC, updateDC) = makeUpdate(4L, d, c, 0, 0) - val (descCE, updateCE) = makeUpdate(5L, c, e, 0, 0) + val (descAB, updateAB) = makeUpdate(1L, a, b, MilliSatoshi(0), 0) + val (descBC, updateBC) = makeUpdate(2L, b, c, MilliSatoshi(0), 0) + val (descAD, updateAD) = makeUpdate(3L, a, d, MilliSatoshi(0), 0) + val (descDC, updateDC) = makeUpdate(4L, d, c, MilliSatoshi(0), 0) + val (descCE, updateCE) = makeUpdate(5L, c, e, MilliSatoshi(0), 0) val graphWithEdges = graph .addEdge(descAB, updateAB) @@ -98,12 +98,12 @@ class GraphSpec extends FunSuite { test("instantiate a graph adding edges only") { - val edgeAB = edgeFromDesc(makeUpdate(1L, a, b, 0, 0)) - val (descBC, updateBC) = makeUpdate(2L, b, c, 0, 0) - val (descAD, updateAD) = makeUpdate(3L, a, d, 0, 0) - val (descDC, updateDC) = makeUpdate(4L, d, c, 0, 0) - val (descCE, updateCE) = makeUpdate(5L, c, e, 0, 0) - val (descBE, updateBE) = makeUpdate(6L, b, e, 0, 0) + val edgeAB = edgeFromDesc(makeUpdate(1L, a, b, MilliSatoshi(0), 0)) + val (descBC, updateBC) = makeUpdate(2L, b, c, MilliSatoshi(0), 0) + val (descAD, updateAD) = makeUpdate(3L, a, d, MilliSatoshi(0), 0) + val (descDC, updateDC) = makeUpdate(4L, d, c, MilliSatoshi(0), 0) + val (descCE, updateCE) = makeUpdate(5L, c, e, MilliSatoshi(0), 0) + val (descBE, updateBE) = makeUpdate(6L, b, e, MilliSatoshi(0), 0) val graph = DirectedGraph(edgeAB) .addEdge(descAD, updateAD) @@ -121,10 +121,10 @@ class GraphSpec extends FunSuite { test("containsEdge should return true if the graph contains that edge, false otherwise") { val updates = Seq( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, c, d, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0) ) val graph = DirectedGraph().addEdges(updates) @@ -144,10 +144,10 @@ class GraphSpec extends FunSuite { val graph = makeTestGraph() - val (descBE, _) = makeUpdate(6L, b, e, 0, 0) - val (descCE, _) = makeUpdate(5L, c, e, 0, 0) - val (descAD, _) = makeUpdate(3L, a, d, 0, 0) - val (descDC, _) = makeUpdate(4L, d, c, 0, 0) + val (descBE, _) = makeUpdate(6L, b, e, MilliSatoshi(0), 0) + val (descCE, _) = makeUpdate(5L, c, e, MilliSatoshi(0), 0) + val (descAD, _) = makeUpdate(3L, a, d, MilliSatoshi(0), 0) + val (descDC, _) = makeUpdate(4L, d, c, MilliSatoshi(0), 0) assert(graph.edgeSet().size === 6) @@ -168,8 +168,8 @@ class GraphSpec extends FunSuite { // contains an edge A --> B val updates = Seq( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0) ) val graph = DirectedGraph().addEdges(updates) @@ -199,19 +199,19 @@ class GraphSpec extends FunSuite { assert(graph.edgesOf(a).size == 2) //now add a new edge a -> b but with a different channel update and a different ShortChannelId - val newEdgeForNewChannel = edgeFromDesc(makeUpdate(15L, a, b, 20, 0)) + val newEdgeForNewChannel = edgeFromDesc(makeUpdate(15L, a, b, MilliSatoshi(20), 0)) val mutatedGraph = graph.addEdge(newEdgeForNewChannel) assert(mutatedGraph.edgesOf(a).size == 3) //if the ShortChannelId is the same we replace the edge and the update, this edge have an update with a different 'feeBaseMsat' - val edgeForTheSameChannel = edgeFromDesc(makeUpdate(15L, a, b, 30, 0)) + val edgeForTheSameChannel = edgeFromDesc(makeUpdate(15L, a, b, MilliSatoshi(30), 0)) val mutatedGraph2 = mutatedGraph.addEdge(edgeForTheSameChannel) assert(mutatedGraph2.edgesOf(a).size == 3) // A --> B , A --> B , A --> D assert(mutatedGraph2.getEdgesBetween(a, b).size === 2) - assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.update.feeBaseMsat === 30) + assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.update.feeBaseMsat === MilliSatoshi(30)) } test("remove a vertex with incoming edges and check those edges are removed too") { @@ -234,11 +234,11 @@ class GraphSpec extends FunSuite { def edgeFromDesc(tuple: (ChannelDesc, ChannelUpdate)): GraphEdge = GraphEdge(tuple._1, tuple._2) def descFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): ChannelDesc = { - makeUpdate(shortChannelId, a, b, 0, 0)._1 + makeUpdate(shortChannelId, a, b, MilliSatoshi(0), 0)._1 } def edgeFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): GraphEdge = { - edgeFromDesc(makeUpdate(shortChannelId, a, b, 0, 0)) + edgeFromDesc(makeUpdate(shortChannelId, a, b, MilliSatoshi(0), 0)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 95b90da7ee..738a1e4e61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{RichWeight, WeightRatios} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} +import fr.acinq.eclair.{Globals, MilliSatoshi, ShortChannelId, randomKey} import org.scalatest.FunSuite import scodec.bits._ @@ -43,10 +43,10 @@ class RouteCalculationSpec extends FunSuite { test("calculate simple route") { val updates = List( - makeUpdate(1L, a, b, 1, 10, cltvDelta = 1), - makeUpdate(2L, b, c, 1, 10, cltvDelta = 1), - makeUpdate(3L, c, d, 1, 10, cltvDelta = 1), - makeUpdate(4L, d, e, 1, 10, cltvDelta = 1) + makeUpdate(1L, a, b, MilliSatoshi(1), 10, cltvDelta = 1), + makeUpdate(2L, b, c, MilliSatoshi(1), 10, cltvDelta = 1), + makeUpdate(3L, c, d, MilliSatoshi(1), 10, cltvDelta = 1), + makeUpdate(4L, d, e, MilliSatoshi(1), 10, cltvDelta = 1) ).toMap val g = makeGraph(updates) @@ -75,43 +75,43 @@ class RouteCalculationSpec extends FunSuite { // cost(AE) = 10007 -> A is source, shortest path found // cost(AB) = 10009 - val amountMsat = 10000 - val expectedCost = 10007 + val amount = MilliSatoshi(10000) + val expectedCost = MilliSatoshi(10007) val updates = List( - makeUpdate(1L, a, b, feeBaseMsat = 1, feeProportionalMillionth = 200, minHtlcMsat = 0), - makeUpdate(4L, a, e, feeBaseMsat = 1, feeProportionalMillionth = 200, minHtlcMsat = 0), - makeUpdate(2L, b, c, feeBaseMsat = 1, feeProportionalMillionth = 300, minHtlcMsat = 0), - makeUpdate(3L, c, d, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0), - makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0), - makeUpdate(6L, f, d, feeBaseMsat = 1, feeProportionalMillionth = 100, minHtlcMsat = 0) + makeUpdate(1L, a, b, feeBase = MilliSatoshi(1), feeProportionalMillionth = 200, minHtlc = MilliSatoshi(0)), + makeUpdate(4L, a, e, feeBase = MilliSatoshi(1), feeProportionalMillionth = 200, minHtlc = MilliSatoshi(0)), + makeUpdate(2L, b, c, feeBase = MilliSatoshi(1), feeProportionalMillionth = 300, minHtlc = MilliSatoshi(0)), + makeUpdate(3L, c, d, feeBase = MilliSatoshi(1), feeProportionalMillionth = 400, minHtlc = MilliSatoshi(0)), + makeUpdate(5L, e, f, feeBase = MilliSatoshi(1), feeProportionalMillionth = 400, minHtlc = MilliSatoshi(0)), + makeUpdate(6L, f, d, feeBase = MilliSatoshi(1), feeProportionalMillionth = 100, minHtlc = MilliSatoshi(0)) ).toMap val graph = makeGraph(updates) - val Success(route) = Router.findRoute(graph, a, d, amountMsat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val Success(route) = Router.findRoute(graph, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) - val totalCost = Graph.pathWeight(hops2Edges(route), amountMsat, false, 0, None).cost + val totalCost = Graph.pathWeight(hops2Edges(route), amount, false, 0, None).cost assert(hops2Ids(route) === 4 :: 5 :: 6 :: Nil) assert(totalCost === expectedCost) // now channel 5 could route the amount (10000) but not the amount + fees (10007) - val (desc, update) = makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0, maxHtlcMsat = Some(10005L)) + val (desc, update) = makeUpdate(5L, e, f, feeBase = MilliSatoshi(1), feeProportionalMillionth = 400, minHtlc = MilliSatoshi(0), maxHtlc = Some(MilliSatoshi(10005L))) val graph1 = graph.addEdge(desc, update) - val Success(route1) = Router.findRoute(graph1, a, d, amountMsat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val Success(route1) = Router.findRoute(graph1, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) assert(hops2Ids(route1) === 1 :: 2 :: 3 :: Nil) } test("calculate route considering the direct channel pays no fees") { val updates = List( - makeUpdate(1L, a, b, 5, 0), // a -> b - makeUpdate(2L, a, d, 15, 0), // a -> d this goes a bit closer to the target and asks for higher fees but is a direct channel - makeUpdate(3L, b, c, 5, 0), // b -> c - makeUpdate(4L, c, d, 5, 0), // c -> d - makeUpdate(5L, d, e, 5, 0) // d -> e + makeUpdate(1L, a, b, MilliSatoshi(5), 0), // a -> b + makeUpdate(2L, a, d, MilliSatoshi(15), 0), // a -> d this goes a bit closer to the target and asks for higher fees but is a direct channel + makeUpdate(3L, b, c, MilliSatoshi(5), 0), // b -> c + makeUpdate(4L, c, d, MilliSatoshi(5), 0), // c -> d + makeUpdate(5L, d, e, MilliSatoshi(5), 0) // d -> e ).toMap val g = makeGraph(updates) @@ -123,10 +123,10 @@ class RouteCalculationSpec extends FunSuite { test("calculate simple route (add and remove edges") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, c, d, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updates) @@ -149,10 +149,10 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 0, 0), - makeUpdate(2L, g, h, 0, 0), - makeUpdate(3L, h, i, 0, 0), - makeUpdate(4L, f, h, 50, 0) // more expensive + makeUpdate(1L, f, g, MilliSatoshi(0), 0), + makeUpdate(2L, g, h, MilliSatoshi(0), 0), + makeUpdate(3L, h, i, MilliSatoshi(0), 0), + makeUpdate(4L, f, h, MilliSatoshi(50), 0) // more expensive ).toMap val graph = makeGraph(updates) @@ -172,10 +172,10 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 0, 0), - makeUpdate(4L, f, i, 50, 0), // our starting node F has a direct channel with I - makeUpdate(2L, g, h, 0, 0), - makeUpdate(3L, h, i, 0, 0) + makeUpdate(1L, f, g, MilliSatoshi(0), 0), + makeUpdate(4L, f, i, MilliSatoshi(50), 0), // our starting node F has a direct channel with I + makeUpdate(2L, g, h, MilliSatoshi(0), 0), + makeUpdate(3L, h, i, MilliSatoshi(0), 0) ).toMap val graph = makeGraph(updates) @@ -193,10 +193,10 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 1, 0), + makeUpdate(1L, f, g, MilliSatoshi(1), 0), // the maximum htlc allowed by this channel is only 50msat greater than what we're sending - makeUpdate(2L, g, h, 1, 0, maxHtlcMsat = Some(DEFAULT_AMOUNT_MSAT + 50)), - makeUpdate(3L, h, i, 1, 0) + makeUpdate(2L, g, h, MilliSatoshi(1), 0, maxHtlc = Some(DEFAULT_AMOUNT_MSAT + MilliSatoshi(50))), + makeUpdate(3L, h, i, MilliSatoshi(1), 0) ).toMap val graph = makeGraph(updates) @@ -214,10 +214,10 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 1, 0), + makeUpdate(1L, f, g, MilliSatoshi(1), 0), // this channel requires a minimum amount that is larger than what we are sending - makeUpdate(2L, g, h, 1, 0, minHtlcMsat = DEFAULT_AMOUNT_MSAT + 50), - makeUpdate(3L, h, i, 1, 0) + makeUpdate(2L, g, h, MilliSatoshi(1), 0, minHtlc = DEFAULT_AMOUNT_MSAT + MilliSatoshi(50)), + makeUpdate(3L, h, i, MilliSatoshi(1), 0) ).toMap val graph = makeGraph(updates) @@ -236,10 +236,10 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 0, 0), - makeUpdate(2L, g, h, 5, 5), // expensive g -> h channel - makeUpdate(6L, g, h, 0, 0), // cheap g -> h channel - makeUpdate(3L, h, i, 0, 0) + makeUpdate(1L, f, g, MilliSatoshi(0), 0), + makeUpdate(2L, g, h, MilliSatoshi(5), 5), // expensive g -> h channel + makeUpdate(6L, g, h, MilliSatoshi(0), 0), // cheap g -> h channel + makeUpdate(3L, h, i, MilliSatoshi(0), 0) ).toMap val graph = makeGraph(updates) @@ -251,11 +251,11 @@ class RouteCalculationSpec extends FunSuite { test("calculate longer but cheaper route") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0), - makeUpdate(5L, b, e, 10, 10) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, c, d, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0), + makeUpdate(5L, b, e, MilliSatoshi(10), 10) ).toMap val g = makeGraph(updates) @@ -267,8 +267,8 @@ class RouteCalculationSpec extends FunSuite { test("no local channels") { val updates = List( - makeUpdate(2L, b, c, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updates) @@ -280,9 +280,9 @@ class RouteCalculationSpec extends FunSuite { test("route not found") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updates) @@ -294,8 +294,8 @@ class RouteCalculationSpec extends FunSuite { test("route not found (source OR target node not connected)") { val updates = List( - makeUpdate(2L, b, c, 0, 0), - makeUpdate(4L, c, d, 0, 0) + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(4L, c, d, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updates).addVertex(a).addVertex(e) @@ -310,15 +310,15 @@ class RouteCalculationSpec extends FunSuite { val lowAmount = DEFAULT_AMOUNT_MSAT / 10 val updatesHi = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0, maxHtlcMsat = Some(DEFAULT_AMOUNT_MSAT)), - makeUpdate(3L, c, d, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0, maxHtlc = Some(DEFAULT_AMOUNT_MSAT)), + makeUpdate(3L, c, d, MilliSatoshi(0), 0) ).toMap val updatesLo = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0, minHtlcMsat = DEFAULT_AMOUNT_MSAT), - makeUpdate(3L, c, d, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0, minHtlc = DEFAULT_AMOUNT_MSAT), + makeUpdate(3L, c, d, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updatesHi) @@ -331,9 +331,9 @@ class RouteCalculationSpec extends FunSuite { test("route to self") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, c, d, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updates) @@ -345,10 +345,10 @@ class RouteCalculationSpec extends FunSuite { test("route to immediate neighbor") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, c, d, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updates) @@ -359,10 +359,10 @@ class RouteCalculationSpec extends FunSuite { test("directed graph") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, c, d, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0) ).toMap // a->e works, e->a fails @@ -380,14 +380,14 @@ class RouteCalculationSpec extends FunSuite { val DUMMY_SIG = Transactions.PlaceHolderSig - val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 0L, 0, 0, 1, 42, 2500, 140, None) - val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 1L, 0, 1, 1, 43, 2501, 141, None) - val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 0, 1, 44, 2502, 142, None) - val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 1, 1, 45, 2503, 143, None) - val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 1, 0, 1, 46, 2504, 144, Some(500000000L)) - val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 0, 1, 1, 47, 2505, 145, None) - val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 0, 1, 48, 2506, 146, None) - val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 1, 1, 49, 2507, 147, None) + val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 0L, 0, 0, 1, MilliSatoshi(42), MilliSatoshi(2500), 140, None) + val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 1L, 0, 1, 1, MilliSatoshi(43), MilliSatoshi(2501), 141, None) + val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 0, 1, MilliSatoshi(44), MilliSatoshi(2502), 142, None) + val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 1, 1, MilliSatoshi(45), MilliSatoshi(2503), 143, None) + val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 1, 0, 1, MilliSatoshi(46), MilliSatoshi(2504), 144, Some(MilliSatoshi(500000000L))) + val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 0, 1, 1, MilliSatoshi(47), MilliSatoshi(2505), 145, None) + val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 0, 1, MilliSatoshi(48), MilliSatoshi(2506), 146, None) + val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 1, 1, MilliSatoshi(49), MilliSatoshi(2507), 147, None) val updates = Map( ChannelDesc(ShortChannelId(1L), a, b) -> uab, @@ -434,10 +434,10 @@ class RouteCalculationSpec extends FunSuite { test("blacklist routes") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, MilliSatoshi(0), 0), + makeUpdate(2L, b, c, MilliSatoshi(0), 0), + makeUpdate(3L, c, d, MilliSatoshi(0), 0), + makeUpdate(4L, d, e, MilliSatoshi(0), 0) ).toMap val g = makeGraph(updates) @@ -446,7 +446,7 @@ class RouteCalculationSpec extends FunSuite { assert(route1.map(hops2Ids) === Failure(RouteNotFound)) // verify that we left the graph untouched - assert(g.containsEdge(makeUpdate(3L, c, d, 0, 0)._1)) // c -> d + assert(g.containsEdge(makeUpdate(3L, c, d, MilliSatoshi(0), 0)._1)) // c -> d assert(g.containsVertex(c)) assert(g.containsVertex(d)) @@ -457,9 +457,9 @@ class RouteCalculationSpec extends FunSuite { test("route to a destination that is not in the graph (with assisted routes)") { val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(3L, c, d, 10, 10) + makeUpdate(1L, a, b, MilliSatoshi(10), 10), + makeUpdate(2L, b, c, MilliSatoshi(10), 10), + makeUpdate(3L, c, d, MilliSatoshi(10), 10) ).toMap val g = makeGraph(updates) @@ -468,7 +468,7 @@ class RouteCalculationSpec extends FunSuite { assert(route.map(hops2Ids) === Failure(RouteNotFound)) // now we add the missing edge to reach the destination - val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5, 5) + val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, MilliSatoshi(5), 5) val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate)) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS) @@ -478,25 +478,25 @@ class RouteCalculationSpec extends FunSuite { test("verify that extra hops takes precedence over known channels") { val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(3L, c, d, 10, 10), - makeUpdate(4L, d, e, 10, 10) + makeUpdate(1L, a, b, MilliSatoshi(10), 10), + makeUpdate(2L, b, c, MilliSatoshi(10), 10), + makeUpdate(3L, c, d, MilliSatoshi(10), 10), + makeUpdate(4L, d, e, MilliSatoshi(10), 10) ).toMap val g = makeGraph(updates) val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) - assert(route1.get(1).lastUpdate.feeBaseMsat == 10) + assert(route1.get(1).lastUpdate.feeBaseMsat == MilliSatoshi(10)) - val (extraDesc, extraUpdate) = makeUpdate(2L, b, c, 5, 5) + val (extraDesc, extraUpdate) = makeUpdate(2L, b, c, MilliSatoshi(5), 5) val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate)) val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS) assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) - assert(route2.get(1).lastUpdate.feeBaseMsat == 5) + assert(route2.get(1).lastUpdate.feeBaseMsat == MilliSatoshi(5)) } test("compute ignored channels") { @@ -519,15 +519,15 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(2L, c, b, 10, 10), - makeUpdate(3L, c, d, 10, 10), - makeUpdate(4L, d, e, 10, 10), - makeUpdate(5L, f, g, 10, 10), - makeUpdate(6L, f, h, 10, 10), - makeUpdate(7L, h, i, 10, 10), - makeUpdate(8L, i, j, 10, 10) + makeUpdate(1L, a, b, MilliSatoshi(10), 10), + makeUpdate(2L, b, c, MilliSatoshi(10), 10), + makeUpdate(2L, c, b, MilliSatoshi(10), 10), + makeUpdate(3L, c, d, MilliSatoshi(10), 10), + makeUpdate(4L, d, e, MilliSatoshi(10), 10), + makeUpdate(5L, f, g, MilliSatoshi(10), 10), + makeUpdate(6L, f, h, MilliSatoshi(10), 10), + makeUpdate(7L, h, i, MilliSatoshi(10), 10), + makeUpdate(8L, i, j, MilliSatoshi(10), 10) ).toMap val ignored = Router.getIgnoredChannelDesc(updates, ignoreNodes = Set(c, j, randomKey.publicKey)) @@ -547,7 +547,7 @@ class RouteCalculationSpec extends FunSuite { val updates = nodes .zip(nodes.drop(1)) // (0, 1) :: (1, 2) :: ... .zipWithIndex // ((0, 1), 0) :: ((1, 2), 1) :: ... - .map { case ((na, nb), index) => makeUpdate(index, na, nb, 5, 0) } + .map { case ((na, nb), index) => makeUpdate(index, na, nb, MilliSatoshi(5), 0) } .toMap val g = makeGraph(updates) @@ -565,10 +565,10 @@ class RouteCalculationSpec extends FunSuite { val updates = nodes .zip(nodes.drop(1)) // (0, 1) :: (1, 2) :: ... .zipWithIndex // ((0, 1), 0) :: ((1, 2), 1) :: ... - .map { case ((na, nb), index) => makeUpdate(index, na, nb, 1, 0) } + .map { case ((na, nb), index) => makeUpdate(index, na, nb, MilliSatoshi(1), 0) } .toMap - val updates2 = updates + makeUpdate(99, nodes(2), nodes(48), 1000, 0) // expensive shorter route + val updates2 = updates + makeUpdate(99, nodes(2), nodes(48), MilliSatoshi(1000), 0) // expensive shorter route val g = makeGraph(updates2) @@ -581,12 +581,12 @@ class RouteCalculationSpec extends FunSuite { val f = randomKey.publicKey val g = makeGraph(List( - makeUpdate(1, a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 50), - makeUpdate(2, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 50), - makeUpdate(3, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 50), - makeUpdate(4, a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(5, e, f, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(6, f, d, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9) + makeUpdate(1, a, b, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 50), + makeUpdate(2, b, c, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 50), + makeUpdate(3, c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 50), + makeUpdate(4, a, e, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(5, e, f, feeBase = MilliSatoshi(5), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(6, f, d, feeBase = MilliSatoshi(5), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9) ).toMap) val route = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = 28)) @@ -598,12 +598,12 @@ class RouteCalculationSpec extends FunSuite { val f = randomKey.publicKey val g = makeGraph(List( - makeUpdate(1, a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(2, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(3, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(4, d, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(5, e, f, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(6, b, f, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9) + makeUpdate(1, a, b, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(2, b, c, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(3, c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(4, d, e, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(5, e, f, feeBase = MilliSatoshi(5), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(6, b, f, feeBase = MilliSatoshi(5), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9) ).toMap) val route = Router.findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3)) @@ -613,11 +613,11 @@ class RouteCalculationSpec extends FunSuite { test("ignore loops") { val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(3L, c, a, 10, 10), - makeUpdate(4L, c, d, 10, 10), - makeUpdate(5L, d, e, 10, 10) + makeUpdate(1L, a, b, MilliSatoshi(10), 10), + makeUpdate(2L, b, c, MilliSatoshi(10), 10), + makeUpdate(3L, c, a, MilliSatoshi(10), 10), + makeUpdate(4L, c, d, MilliSatoshi(10), 10), + makeUpdate(5L, d, e, MilliSatoshi(10), 10) ).toMap val g = makeGraph(updates) @@ -630,13 +630,13 @@ class RouteCalculationSpec extends FunSuite { // the graph contains a possible 0-cost path that goes back on its steps ( e -> f, f -> e ) val updates = List( - makeUpdate(1L, a, b, 10, 10), // a -> b - makeUpdate(2L, b, c, 10, 10), - makeUpdate(4L, c, d, 10, 10), - makeUpdate(3L, b, e, 0, 0), // b -> e - makeUpdate(6L, e, f, 0, 0), // e -> f - makeUpdate(6L, f, e, 0, 0), // e <- f - makeUpdate(5L, e, d, 0, 0) // e -> d + makeUpdate(1L, a, b, MilliSatoshi(10), 10), // a -> b + makeUpdate(2L, b, c, MilliSatoshi(10), 10), + makeUpdate(4L, c, d, MilliSatoshi(10), 10), + makeUpdate(3L, b, e, MilliSatoshi(0), 0), // b -> e + makeUpdate(6L, e, f, MilliSatoshi(0), 0), // e -> f + makeUpdate(6L, f, e, MilliSatoshi(0), 0), // e <- f + makeUpdate(5L, e, d, MilliSatoshi(0), 0) // e -> d ).toMap val g = makeGraph(updates) @@ -671,13 +671,13 @@ class RouteCalculationSpec extends FunSuite { val edges = Seq( - makeUpdate(1L, d, a, 1, 0), - makeUpdate(2L, d, e, 1, 0), - makeUpdate(3L, a, e, 1, 0), - makeUpdate(4L, e, b, 1, 0), - makeUpdate(5L, e, f, 1, 0), - makeUpdate(6L, b, c, 1, 0), - makeUpdate(7L, c, f, 1, 0) + makeUpdate(1L, d, a, MilliSatoshi(1), 0), + makeUpdate(2L, d, e, MilliSatoshi(1), 0), + makeUpdate(3L, a, e, MilliSatoshi(1), 0), + makeUpdate(4L, e, b, MilliSatoshi(1), 0), + makeUpdate(5L, e, f, MilliSatoshi(1), 0), + makeUpdate(6L, b, c, MilliSatoshi(1), 0), + makeUpdate(7L, c, f, MilliSatoshi(1), 0) ).toMap val graph = DirectedGraph.makeGraph(edges) @@ -703,15 +703,15 @@ class RouteCalculationSpec extends FunSuite { val edges = Seq( - makeUpdate(10L, c, e, 2, 0), - makeUpdate(20L, c, d, 3, 0), - makeUpdate(30L, d, f, 4, 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route - makeUpdate(40L, e, d, 1, 0), - makeUpdate(50L, e, f, 2, 0), - makeUpdate(60L, e, g, 3, 0), - makeUpdate(70L, f, g, 2, 0), - makeUpdate(80L, f, h, 1, 0), - makeUpdate(90L, g, h, 2, 0) + makeUpdate(10L, c, e, MilliSatoshi(2), 0), + makeUpdate(20L, c, d, MilliSatoshi(3), 0), + makeUpdate(30L, d, f, MilliSatoshi(4), 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route + makeUpdate(40L, e, d, MilliSatoshi(1), 0), + makeUpdate(50L, e, f, MilliSatoshi(2), 0), + makeUpdate(60L, e, g, MilliSatoshi(3), 0), + makeUpdate(70L, f, g, MilliSatoshi(2), 0), + makeUpdate(80L, f, h, MilliSatoshi(1), 0), + makeUpdate(90L, g, h, MilliSatoshi(2), 0) ) val graph = DirectedGraph().addEdges(edges) @@ -732,19 +732,19 @@ class RouteCalculationSpec extends FunSuite { // simple graph with only 2 possible paths from A to F val edges = Seq( - makeUpdate(1L, a, b, 1, 0), - makeUpdate(1L, b, a, 1, 0), - makeUpdate(2L, b, c, 1, 0), - makeUpdate(2L, c, b, 1, 0), - makeUpdate(3L, c, f, 1, 0), - makeUpdate(3L, f, c, 1, 0), - makeUpdate(4L, c, d, 1, 0), - makeUpdate(4L, d, c, 1, 0), - makeUpdate(41L, d, c, 1, 0), // there is more than one D -> C channel - makeUpdate(5L, d, e, 1, 0), - makeUpdate(5L, e, d, 1, 0), - makeUpdate(6L, e, f, 1, 0), - makeUpdate(6L, f, e, 1, 0) + makeUpdate(1L, a, b, MilliSatoshi(1), 0), + makeUpdate(1L, b, a, MilliSatoshi(1), 0), + makeUpdate(2L, b, c, MilliSatoshi(1), 0), + makeUpdate(2L, c, b, MilliSatoshi(1), 0), + makeUpdate(3L, c, f, MilliSatoshi(1), 0), + makeUpdate(3L, f, c, MilliSatoshi(1), 0), + makeUpdate(4L, c, d, MilliSatoshi(1), 0), + makeUpdate(4L, d, c, MilliSatoshi(1), 0), + makeUpdate(41L, d, c, MilliSatoshi(1), 0), // there is more than one D -> C channel + makeUpdate(5L, d, e, MilliSatoshi(1), 0), + makeUpdate(5L, e, d, MilliSatoshi(1), 0), + makeUpdate(6L, e, f, MilliSatoshi(1), 0), + makeUpdate(6L, f, e, MilliSatoshi(1), 0) ) val graph = DirectedGraph().addEdges(edges) @@ -759,19 +759,19 @@ class RouteCalculationSpec extends FunSuite { test("select a random route below the requested fee") { - val strictFeeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBaseMsat = 7, maxFeePct = 0) + val strictFeeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = MilliSatoshi(7), maxFeePct = 0) // A -> B -> C -> D has total cost of 10000005 // A -> E -> C -> D has total cost of 11080003 !! // A -> E -> F -> D has total cost of 10000006 val g = makeGraph(List( - makeUpdate(1L, a, b, feeBaseMsat = 1, 0), - makeUpdate(4L, a, e, feeBaseMsat = 1, 0), - makeUpdate(2L, b, c, feeBaseMsat = 2, 0), - makeUpdate(3L, c, d, feeBaseMsat = 3, 0), - makeUpdate(5L, e, f, feeBaseMsat = 3, 0), - makeUpdate(6L, f, d, feeBaseMsat = 3, 0), - makeUpdate(7L, e, c, feeBaseMsat = 9, 0) + makeUpdate(1L, a, b, feeBase = MilliSatoshi(1), 0), + makeUpdate(4L, a, e, feeBase = MilliSatoshi(1), 0), + makeUpdate(2L, b, c, feeBase = MilliSatoshi(2), 0), + makeUpdate(3L, c, d, feeBase = MilliSatoshi(3), 0), + makeUpdate(5L, e, f, feeBase = MilliSatoshi(3), 0), + makeUpdate(6L, f, d, feeBase = MilliSatoshi(3), 0), + makeUpdate(7L, e, c, feeBase = MilliSatoshi(9), 0) ).toMap) (for {_ <- 0 to 10} yield Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3, routeParams = strictFeeParams)).map { @@ -781,25 +781,25 @@ class RouteCalculationSpec extends FunSuite { val routeCost = Graph.pathWeight(hops2Edges(someRoute), DEFAULT_AMOUNT_MSAT, isPartial = false, 0, None).cost - DEFAULT_AMOUNT_MSAT // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7msat of fees) - assert(routeCost == 5 || routeCost == 6) + assert(routeCost == MilliSatoshi(5) || routeCost == MilliSatoshi(6)) } } test("Use weight ratios to when computing the edge weight") { - val largeCapacity = 8000000000L + val largeCapacity = MilliSatoshi(8000000000L) // A -> B -> C -> D is 'fee optimized', lower fees route (totFees = 2, totCltv = 4000) // A -> E -> F -> D is 'timeout optimized', lower CLTV route (totFees = 3, totCltv = 18) // A -> E -> C -> D is 'capacity optimized', more recent channel/larger capacity route val updates = List( - makeUpdate(1L, a, b, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 13), - makeUpdate(4L, a, e, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdate(2L, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(3L, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(5L, e, f, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(6L, f, d, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(7L, e, c, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = Some(largeCapacity), cltvDelta = 12) + makeUpdate(1L, a, b, feeBase = MilliSatoshi(0), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 13), + makeUpdate(4L, a, e, feeBase = MilliSatoshi(0), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12), + makeUpdate(2L, b, c, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 500), + makeUpdate(3L, c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 500), + makeUpdate(5L, e, f, feeBase = MilliSatoshi(2), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(6L, f, d, feeBase = MilliSatoshi(2), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 9), + makeUpdate(7L, e, c, feeBase = MilliSatoshi(2), 0, minHtlc = MilliSatoshi(0), maxHtlc = Some(largeCapacity), cltvDelta = 12) ).toMap val g = makeGraph(updates) @@ -829,12 +829,12 @@ class RouteCalculationSpec extends FunSuite { val currentBlockHeight = 554000 val g = makeGraph(List( - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x1"), a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x4"), a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x2"), b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), // younger channel - makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x3"), c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x5"), e, f, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x6"), f, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144) + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x1"), a, b, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x4"), a, e, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x2"), b, c, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), // younger channel + makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x3"), c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x5"), e, f, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x6"), f, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144) ).toMap) Globals.blockCount.set(currentBlockHeight) @@ -851,12 +851,12 @@ class RouteCalculationSpec extends FunSuite { test("prefer a route with a smaller total CLTV if fees and score are the same") { val g = makeGraph(List( - makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 10), // smaller CLTV - makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12) + makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12), + makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12), + makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 10), // smaller CLTV + makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12), + makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12), + makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 12) ).toMap) @@ -875,12 +875,12 @@ class RouteCalculationSpec extends FunSuite { // A -> B -> C -> D is cheaper but has a total CLTV > 2016! // A -> E -> F -> D is more expensive but has a total CLTV < 2016 val g = makeGraph(List( - makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 1000), - makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 900), - makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBaseMsat = 10, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBaseMsat = 10, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144) + makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), + makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), + makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 1000), + makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 900), + makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144), + makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(0), maxHtlc = None, cltvDelta = 144) ).toMap) val Success(routeScoreOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( @@ -897,22 +897,22 @@ class RouteCalculationSpec extends FunSuite { // This test have a channel (542280x2156x0) that according to heuristics is very convenient but actually useless to reach the target, // then if the cost function is not monotonic the path-finding breaks because the result path contains a loop. val updates = List( - ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = 0, feeBaseMsat = 1000, 100, Some(15000000000L)), - ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 0.toByte, 14, htlcMinimumMsat = 1, 1000, 10, Some(4294967295L)), - ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = 1000, feeBaseMsat = 1000, 100, Some(16777000000L)), - ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = 1, 667, 1, Some(16777000000L)), - ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 3.toByte, 144, htlcMinimumMsat = 1, 1000, 100, Some(230000000L)), - ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = 1, 1000, 100, Some(230000000L)) + ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(1000), 100, Some(MilliSatoshi(15000000000L))), + ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 0.toByte, 14, htlcMinimumMsat = MilliSatoshi(1), MilliSatoshi(1000), 10, Some(MilliSatoshi(4294967295L))), + ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = MilliSatoshi(1000), feeBaseMsat = MilliSatoshi(1000), 100, Some(MilliSatoshi(16777000000L))), + ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = MilliSatoshi(1), MilliSatoshi(667), 1, Some(MilliSatoshi(16777000000L))), + ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 3.toByte, 144, htlcMinimumMsat = MilliSatoshi(1), MilliSatoshi(1000), 100, Some(MilliSatoshi(230000000L))), + ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = MilliSatoshi(1), MilliSatoshi(1000), 100, Some(MilliSatoshi(230000000L))) ).toMap val g = DirectedGraph.makeGraph(updates) - val params = RouteParams(randomize = false, maxFeeBaseMsat = 21000, maxFeePct = 0.03, routeMaxCltv = 1008, routeMaxLength = 6, ratios = Some( + val params = RouteParams(randomize = false, maxFeeBase = MilliSatoshi(21000), maxFeePct = 0.03, routeMaxCltv = 1008, routeMaxLength = 6, ratios = Some( WeightRatios(cltvDeltaFactor = 0.15, ageFactor = 0.35, capacityFactor = 0.5) )) val thisNode = PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96") val targetNode = PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca") - val amount = 351000 + val amount = MilliSatoshi(351000) Globals.blockCount.set(567634) // simulate mainnet block for heuristic val Success(route) = Router.findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, params) @@ -927,9 +927,9 @@ object RouteCalculationSpec { val noopBoundaries = { _: RichWeight => true } - val DEFAULT_AMOUNT_MSAT = 10000000 + val DEFAULT_AMOUNT_MSAT = MilliSatoshi(10000000) - val DEFAULT_ROUTE_PARAMS = RouteParams(randomize = false, maxFeeBaseMsat = 21000, maxFeePct = 0.03, routeMaxCltv = 2016, routeMaxLength = 6, ratios = None) + val DEFAULT_ROUTE_PARAMS = RouteParams(randomize = false, maxFeeBase = MilliSatoshi(21000), maxFeePct = 0.03, routeMaxCltv = 2016, routeMaxLength = 6, ratios = None) val DUMMY_SIG = Transactions.PlaceHolderSig @@ -938,26 +938,26 @@ object RouteCalculationSpec { ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, ByteVector.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey.publicKey, randomKey.publicKey) } - def makeUpdate(shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, feeBaseMsat: Int, feeProportionalMillionth: Int, minHtlcMsat: Long = DEFAULT_AMOUNT_MSAT, maxHtlcMsat: Option[Long] = None, cltvDelta: Int = 0): (ChannelDesc, ChannelUpdate) = { - makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBaseMsat, feeProportionalMillionth, minHtlcMsat, maxHtlcMsat, cltvDelta) + def makeUpdate(shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: Int = 0): (ChannelDesc, ChannelUpdate) = { + makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc, cltvDelta) } - def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBaseMsat: Int, feeProportionalMillionth: Int, minHtlcMsat: Long = DEFAULT_AMOUNT_MSAT, maxHtlcMsat: Option[Long] = None, cltvDelta: Int = 0, timestamp: Long = 0): (ChannelDesc, ChannelUpdate) = + def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: Int = 0, timestamp: Long = 0): (ChannelDesc, ChannelUpdate) = ChannelDesc(shortChannelId, nodeId1, nodeId2) -> ChannelUpdate( signature = DUMMY_SIG, chainHash = Block.RegtestGenesisBlock.hash, shortChannelId = shortChannelId, timestamp = timestamp, - messageFlags = maxHtlcMsat match { + messageFlags = maxHtlc match { case Some(_) => 1 case None => 0 }, channelFlags = 0, cltvExpiryDelta = cltvDelta, - htlcMinimumMsat = minHtlcMsat, - feeBaseMsat = feeBaseMsat, + htlcMinimumMsat = minHtlc, + feeBaseMsat = feeBase, feeProportionalMillionths = feeProportionalMillionth, - htlcMaximumMsat = maxHtlcMsat + htlcMaximumMsat = maxHtlc ) def makeGraph(updates: Map[ChannelDesc, ChannelUpdate]) = DirectedGraph.makeGraph(updates) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 34d5478c29..713599fc46 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.router.Announcements.makeChannelUpdate import fr.acinq.eclair.router.RouteCalculationSpec.DEFAULT_AMOUNT_MSAT import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.QueryShortChannelIds -import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} +import fr.acinq.eclair.{Globals, MilliSatoshi, ShortChannelId, randomKey} import scodec.bits._ import scala.compat.Platform @@ -51,21 +51,21 @@ class RouterSpec extends BaseRouterSpec { val channelId_ac = ShortChannelId(420000, 5, 0) val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c) - val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId_ac, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId_ac, cltvExpiryDelta = 7, MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, MilliSatoshi(500000000L)) // a-x will not be found val priv_x = randomKey val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey) - val update_ax = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_x.publicKey, chan_ax.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_ax = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_x.publicKey, chan_ax.shortChannelId, cltvExpiryDelta = 7, MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, MilliSatoshi(500000000L)) // a-y will have an invalid script val priv_y = randomKey val priv_funding_y = randomKey val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) - val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, cltvExpiryDelta = 7, MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, MilliSatoshi(500000000L)) // a-z will be spent val priv_z = randomKey val priv_funding_z = randomKey val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) - val update_az = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_z.publicKey, chan_az.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_az = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_z.publicKey, chan_az.shortChannelId, cltvExpiryDelta = 7, MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, MilliSatoshi(500000000L)) router ! PeerRoutingMessage(null, remoteNodeId, chan_ac) router ! PeerRoutingMessage(null, remoteNodeId, chan_ax) @@ -200,7 +200,7 @@ class RouterSpec extends BaseRouterSpec { assert(res.hops.map(_.nodeId).toList === a :: b :: c :: Nil) assert(res.hops.last.nextNodeId === d) - val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, 0, feeBaseMsat = 153000, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L, enable = false) + val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, MilliSatoshi(0), feeBaseMsat = MilliSatoshi(153000), feeProportionalMillionths = 4, htlcMaximumMsat = MilliSatoshi(500000000L), enable = false) sender.send(router, PeerRoutingMessage(null, remoteNodeId, channelUpdate_cd1)) sender.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1)) sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams)) @@ -256,7 +256,7 @@ class RouterSpec extends BaseRouterSpec { val channelId = ShortChannelId(blockHeight, 5, 0) val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) val timestamp = (Platform.currentTime.milliseconds - 14.days - 1.day).toSeconds - val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 5, timestamp = timestamp) + val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, htlcMaximumMsat = MilliSatoshi(5), timestamp = timestamp) val probe = TestProbe() probe.ignoreMsg { case _: TransportHandler.ReadAck => true } probe.send(router, PeerRoutingMessage(null, remoteNodeId, announcement)) @@ -270,7 +270,7 @@ class RouterSpec extends BaseRouterSpec { val state = sender.expectMsgType[RoutingState] - val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L, timestamp = Platform.currentTime.millisecond.toSeconds) + val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, htlcMaximumMsat = MilliSatoshi(500000000L), timestamp = Platform.currentTime.millisecond.toSeconds) // we want to make sure that transport receives the query val transport = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 928d9aeba3..22e19db62c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -285,8 +285,8 @@ object RoutingSyncSpec { pub2priv += (priv1.publicKey -> priv1) pub2priv += (priv2.publicKey -> priv2) val channelAnn_12 = channelAnnouncement(shortChannelId, priv1, priv2, priv_funding1, priv_funding2) - val channelUpdate_12 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv1, priv2.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) - val channelUpdate_21 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv2, priv1.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = timestamp) + val channelUpdate_12 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv1, priv2.publicKey, shortChannelId, cltvExpiryDelta = 7, MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, MilliSatoshi(500000000L), timestamp = timestamp) + val channelUpdate_21 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv2, priv1.publicKey, shortChannelId, cltvExpiryDelta = 7, MilliSatoshi(0), feeBaseMsat = MilliSatoshi(766000), feeProportionalMillionths = 10, MilliSatoshi(500000000L), timestamp = timestamp) val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "", Color(0, 0, 0), List()) val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "", Color(0, 0, 0), List()) (channelAnn_12, channelUpdate_12, channelUpdate_21, nodeAnnouncement_1, nodeAnnouncement_2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index 1b95b50856..3df953ab0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -17,53 +17,53 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.{ByteVector32, Crypto} -import fr.acinq.eclair.{TestConstants, randomBytes32} +import fr.acinq.eclair.{MilliSatoshi, TestConstants, randomBytes32} import fr.acinq.eclair.wire.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import org.scalatest.FunSuite class CommitmentSpecSpec extends FunSuite { test("add, fulfill and fail htlcs from the sender side") { - val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocalMsat = 5000 * 1000, toRemoteMsat = 0) + val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocal = MilliSatoshi(5000000), toRemote = MilliSatoshi(0)) val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000 * 1000), H, 400, TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, add1 :: Nil, Nil) - assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(OUT, add1)), toLocalMsat = 3000 * 1000)) + assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(OUT, add1)), toLocal = MilliSatoshi(3000000))) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(1000 * 1000), H, 400, TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, add2 :: Nil, Nil) - assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(OUT, add1), DirectedHtlc(OUT, add2)), toLocalMsat = 2000 * 1000)) + assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(OUT, add1), DirectedHtlc(OUT, add2)), toLocal = MilliSatoshi(2000000))) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, Nil, ful1 :: Nil) - assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(OUT, add2)), toRemoteMsat = 2000 * 1000)) + assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(OUT, add2)), toRemote = MilliSatoshi(2000000))) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, Nil, fail1 :: Nil) - assert(spec4 === spec3.copy(htlcs = Set(), toLocalMsat = 3000 * 1000)) + assert(spec4 === spec3.copy(htlcs = Set(), toLocal = MilliSatoshi(3000000))) } test("add, fulfill and fail htlcs from the receiver side") { - val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocalMsat = 0, toRemoteMsat = 5000 * 1000) + val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocal = MilliSatoshi(0), toRemote = MilliSatoshi(5000 * 1000)) val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000 * 1000), H, 400, TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) - assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(IN, add1)), toRemoteMsat = 3000 * 1000)) + assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(IN, add1)), toRemote = MilliSatoshi(3000 * 1000))) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(1000 * 1000), H, 400, TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) - assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(IN, add1), DirectedHtlc(IN, add2)), toRemoteMsat = 2000 * 1000)) + assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(IN, add1), DirectedHtlc(IN, add2)), toRemote = MilliSatoshi(2000 * 1000))) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, ful1 :: Nil, Nil) - assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(IN, add2)), toLocalMsat = 2000 * 1000)) + assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(IN, add2)), toLocal = MilliSatoshi(2000 * 1000))) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, fail1 :: Nil, Nil) - assert(spec4 === spec3.copy(htlcs = Set(), toRemoteMsat = 3000 * 1000)) + assert(spec4 === spec3.copy(htlcs = Set(), toRemote = MilliSatoshi(3000 * 1000))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 013d70b8fb..d52e1f13cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin._ -import fr.acinq.eclair.TestConstants +import fr.acinq.bitcoin +import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, Script, ScriptFlags, Transaction} +import fr.acinq.eclair.{MilliSatoshi, TestConstants} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} @@ -154,11 +155,11 @@ class TestVectorsSpec extends FunSuite with Logging { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000), Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000), Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000), Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(3000000), Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(4000000), Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) ) val htlcScripts = htlcs.map(htlc => htlc.direction match { case OUT => Scripts.htlcOffered(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(htlc.add.paymentHash)) @@ -178,8 +179,8 @@ class TestVectorsSpec extends FunSuite with Logging { } def run(spec: CommitmentSpec) = { - logger.info(s"to_local_msat: ${spec.toLocalMsat}") - logger.info(s"to_remote_msat: ${spec.toRemoteMsat}") + logger.info(s"to_local_msat: ${spec.toLocal}") + logger.info(s"to_remote_msat: ${spec.toRemote}") logger.info(s"local_feerate_per_kw: ${spec.feeratePerKw}") val commitTx = { @@ -286,7 +287,7 @@ class TestVectorsSpec extends FunSuite with Logging { test("simple commitment tx with no HTLCs") { val name = "simple commitment tx with no HTLCs" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = 15000, toLocalMsat = 7000000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = 15000, toLocal = MilliSatoshi(7000000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 2) @@ -297,7 +298,7 @@ class TestVectorsSpec extends FunSuite with Logging { test("commitment tx with all 5 htlcs untrimmed (minimum feerate)") { val name = "commitment tx with all 5 htlcs untrimmed (minimum feerate)" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 0, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 0, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 7) @@ -308,7 +309,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 7 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 7) @@ -322,7 +323,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 6 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 6) @@ -336,7 +337,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 6 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 6) @@ -350,7 +351,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 5 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 5) @@ -364,7 +365,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 5 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 5) @@ -378,7 +379,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 4 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 4) @@ -392,7 +393,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 4 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 4) @@ -406,7 +407,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 3 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 3) @@ -420,7 +421,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 3 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 3) @@ -434,7 +435,7 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 2 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 2) @@ -447,7 +448,7 @@ class TestVectorsSpec extends FunSuite with Logging { test("commitment tx with 2 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 2 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651180, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651180, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 2) @@ -460,7 +461,7 @@ class TestVectorsSpec extends FunSuite with Logging { test("commitment tx with 1 output untrimmed (minimum feerate)") { val name = "commitment tx with 1 output untrimmed (minimum feerate)" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651181, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651181, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 1) @@ -473,7 +474,7 @@ class TestVectorsSpec extends FunSuite with Logging { test("commitment tx with fee greater than funder amount") { val name = "commitment tx with fee greater than funder amount" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651936, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651936, toLocal = MilliSatoshi(6988000000L), toRemote = MilliSatoshi(3000000000L)) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index f3819c8bc8..e323788552 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -20,9 +20,10 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, Protocol, Satoshi, Script, Transaction, TxOut, millibtc2satoshi} import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.{TestConstants, randomBytes32} +import fr.acinq.eclair.{MilliSatoshi, TestConstants, randomBytes32} +import fr.acinq.eclair._ import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} import fr.acinq.eclair.wire.UpdateAddHtlc @@ -63,12 +64,12 @@ class TransactionsSpec extends FunSuite with Logging { test("compute fees") { // see BOLT #3 specs val htlcs = Set( - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000).amount, ByteVector32.Zeroes, 552, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, ByteVector32.Zeroes, 553, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(7000000).amount, ByteVector32.Zeroes, 550, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000).amount, ByteVector32.Zeroes, 551, TestConstants.emptyOnionPacket)) + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000), ByteVector32.Zeroes, 552, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000), ByteVector32.Zeroes, 553, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(7000000), ByteVector32.Zeroes, 550, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000), ByteVector32.Zeroes, 551, TestConstants.emptyOnionPacket)) ) - val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocalMsat = 0, toRemoteMsat = 0) + val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocal = MilliSatoshi(0), toRemote = MilliSatoshi(0)) val fee = Transactions.commitTxFee(Satoshi(546), spec) assert(fee == Satoshi(5340)) } @@ -125,10 +126,10 @@ class TransactionsSpec extends FunSuite with Logging { // HtlcPenaltyTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(20000 * 1000), sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry) val pubKeyScript = write(pay2wsh(redeemScript)) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx, outputsAlreadyUsed = Set.empty, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(htlcPenaltyTx, PlaceHolderSig, localRevocationPriv.publicKey).tx) @@ -140,9 +141,9 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcSuccessTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(20000 * 1000), sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash)))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcSuccessTx, PlaceHolderSig, paymentPreimage).tx) @@ -154,9 +155,9 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcTimeoutTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(20000 * 1000), sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val claimClaimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimClaimHtlcTimeoutTx, PlaceHolderSig).tx) @@ -183,14 +184,14 @@ class TransactionsSpec extends FunSuite with Logging { // htlc1 and htlc2 are regular IN/OUT htlcs val paymentPreimage1 = randomBytes32 - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, millibtc2satoshi(MilliBtc(100)).amount * 1000, sha256(paymentPreimage1), 300, TestConstants.emptyOnionPacket) + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimage1), 300, TestConstants.emptyOnionPacket) val paymentPreimage2 = randomBytes32 - val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, millibtc2satoshi(MilliBtc(200)).amount * 1000, sha256(paymentPreimage2), 300, TestConstants.emptyOnionPacket) + val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(200).toMilliSatoshi, sha256(paymentPreimage2), 300, TestConstants.emptyOnionPacket) // htlc3 and htlc4 are dust htlcs IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage val paymentPreimage3 = randomBytes32 - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).amount * 1000, sha256(paymentPreimage3), 300, TestConstants.emptyOnionPacket) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), 300, TestConstants.emptyOnionPacket) val paymentPreimage4 = randomBytes32 - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).amount * 1000, sha256(paymentPreimage4), 300, TestConstants.emptyOnionPacket) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), 300, TestConstants.emptyOnionPacket) val spec = CommitmentSpec( htlcs = Set( DirectedHtlc(OUT, htlc1), @@ -199,8 +200,8 @@ class TransactionsSpec extends FunSuite with Logging { DirectedHtlc(IN, htlc4) ), feeratePerKw = feeratePerKw, - toLocalMsat = millibtc2satoshi(MilliBtc(400)).amount * 1000, - toRemoteMsat = millibtc2satoshi(MilliBtc(300)).amount * 1000) + toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, + toRemote = millibtc2satoshi(MilliBtc(300)).toMilliSatoshi) val commitTxNumber = 0x404142434445L val commitTx = { @@ -320,7 +321,7 @@ class TransactionsSpec extends FunSuite with Logging { } def htlc(direction: Direction, amount: Satoshi): DirectedHtlc = - DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.amount * 1000, ByteVector32.Zeroes, 144, TestConstants.emptyOnionPacket)) + DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, 144, TestConstants.emptyOnionPacket)) test("BOLT 2 fee tests") { @@ -353,7 +354,7 @@ class TransactionsSpec extends FunSuite with Logging { case "received" => htlc(IN, Satoshi(amount.toLong)) } }).toSet - TestSetup(name, dustLimit, CommitmentSpec(htlcs = htlcs, feeratePerKw = feerate_per_kw.toLong, toLocalMsat = to_local_msat.toLong, toRemoteMsat = to_remote_msat.toLong), Satoshi(fee.toLong)) + TestSetup(name, dustLimit, CommitmentSpec(htlcs = htlcs, feeratePerKw = feerate_per_kw.toLong, toLocal = MilliSatoshi(to_local_msat.toLong), toRemote = MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong)) }) // simple non-reg test making sure we are not missing tests 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 81f6e6e0d6..c5a0af0449 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 @@ -21,7 +21,7 @@ import java.util.UUID import akka.actor.ActorSystem import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, MilliSatoshi, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.api.JsonSupport import fr.acinq.eclair.channel.Helpers.Funding @@ -84,10 +84,10 @@ class ChannelCodecsSpec extends FunSuite { val o = LocalParams( nodeId = randomKey.publicKey, channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), - dustLimitSatoshis = Random.nextInt(Int.MaxValue), + dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserveSatoshis = Random.nextInt(Int.MaxValue), - htlcMinimumMsat = Random.nextInt(Int.MaxValue), + channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = Random.nextInt(Short.MaxValue), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), @@ -102,10 +102,10 @@ class ChannelCodecsSpec extends FunSuite { test("encode/decode remoteparams") { val o = RemoteParams( nodeId = randomKey.publicKey, - dustLimitSatoshis = Random.nextInt(Int.MaxValue), + dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserveSatoshis = Random.nextInt(Int.MaxValue), - htlcMinimumMsat = Random.nextInt(Int.MaxValue), + channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = Random.nextInt(Short.MaxValue), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), fundingPubKey = randomKey.publicKey, @@ -129,7 +129,7 @@ class ChannelCodecsSpec extends FunSuite { val add = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), - amountMsat = Random.nextInt(Int.MaxValue), + amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) @@ -143,14 +143,14 @@ class ChannelCodecsSpec extends FunSuite { val add1 = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), - amountMsat = Random.nextInt(Int.MaxValue), + amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) val add2 = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), - amountMsat = Random.nextInt(Int.MaxValue), + amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) @@ -161,8 +161,8 @@ class ChannelCodecsSpec extends FunSuite { val o = CommitmentSpec( htlcs = Set(htlc1, htlc2), feeratePerKw = Random.nextInt(Int.MaxValue), - toLocalMsat = Random.nextInt(Int.MaxValue), - toRemoteMsat = Random.nextInt(Int.MaxValue) + toLocal = MilliSatoshi(Random.nextInt(Int.MaxValue)), + toRemote = MilliSatoshi(Random.nextInt(Int.MaxValue)) ) val encoded = commitmentSpecCodec.encode(o).require val decoded = commitmentSpecCodec.decode(encoded).require @@ -173,17 +173,17 @@ class ChannelCodecsSpec extends FunSuite { val id = UUID.randomUUID() assert(originCodec.decodeValue(originCodec.encode(Local(id, Some(ActorSystem("system").deadLetters))).require).require === Local(id, None)) // TODO: add backward compatibility check - val relayed = Relayed(randomBytes32, 4324, 12000000L, 11000000L) + val relayed = Relayed(randomBytes32, 4324, MilliSatoshi(12000000L), MilliSatoshi(11000000L)) assert(originCodec.decodeValue(originCodec.encode(relayed).require).require === relayed) } test("encode/decode map of origins") { val map = Map( 1L -> Local(UUID.randomUUID(), None), - 42L -> Relayed(randomBytes32, 4324, 12000000L, 11000000L), - 130L -> Relayed(randomBytes32, -45, 13000000L, 12000000L), - 1000L -> Relayed(randomBytes32, 10, 14000000L, 13000000L), - -32L -> Relayed(randomBytes32, 54, 15000000L, 14000000L), + 42L -> Relayed(randomBytes32, 4324, MilliSatoshi(12000000L), MilliSatoshi(11000000L)), + 130L -> Relayed(randomBytes32, -45, MilliSatoshi(13000000L), MilliSatoshi(12000000L)), + 1000L -> Relayed(randomBytes32, 10, MilliSatoshi(14000000L), MilliSatoshi(13000000L)), + -32L -> Relayed(randomBytes32, 54, MilliSatoshi(15000000L), MilliSatoshi(14000000L)), -4L -> Local(UUID.randomUUID(), None)) assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map) } @@ -313,8 +313,24 @@ class ChannelCodecsSpec extends FunSuite { // and we decode with the new codec val newnormal = stateDataCodec.decode(newbin.bits).require.value // finally we check that the actual data is the same as before (we just remove the new json field) - val oldjson = Serialization.write(oldnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") - val newjson = Serialization.write(newnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") + val oldjson = Serialization.write(oldnormal)(JsonSupport.formats) + .replace(""","unknownFields":""""", "") + .replace(""""channelVersion":"00000000000000000000000000000000",""", "") + .replace(""""dustLimit"""", """"dustLimitSatoshis"""") + .replace(""""channelReserve"""", """"channelReserveSatoshis"""") + .replace(""""htlcMinimum"""", """"htlcMinimumMsat"""") + .replace(""""toLocal"""", """"toLocalMsat"""") + .replace(""""toRemote"""", """"toRemoteMsat"""") + + val newjson = Serialization.write(newnormal)(JsonSupport.formats) + .replace(""","unknownFields":""""", "") + .replace(""""channelVersion":"00000000000000000000000000000000",""", "") + .replace(""""dustLimit"""", """"dustLimitSatoshis"""") + .replace(""""channelReserve"""", """"channelReserveSatoshis"""") + .replace(""""htlcMinimum"""", """"htlcMinimumMsat"""") + .replace(""""toLocal"""", """"toLocalMsat"""") + .replace(""""toRemote"""", """"toRemoteMsat"""") + assert(oldjson === refjson) assert(newjson === refjson) } @@ -328,10 +344,10 @@ object ChannelCodecsSpec { val localParams = LocalParams( keyManager.nodeId, channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), - dustLimitSatoshis = Satoshi(546).toLong, + dustLimit = Satoshi(546), maxHtlcValueInFlightMsat = UInt64(50000000), - channelReserveSatoshis = 10000, - htlcMinimumMsat = 10000, + channelReserve = Satoshi(10000), + htlcMinimum = MilliSatoshi(10000), toSelfDelay = 144, maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, @@ -341,10 +357,10 @@ object ChannelCodecsSpec { val remoteParams = RemoteParams( nodeId = randomKey.publicKey, - dustLimitSatoshis = Satoshi(546).toLong, + dustLimit = Satoshi(546), maxHtlcValueInFlightMsat = UInt64(5000000), - channelReserveSatoshis = 10000, - htlcMinimumMsat = 5000, + channelReserve = Satoshi(10000), + htlcMinimum = MilliSatoshi(5000), toSelfDelay = 144, maxAcceptedHtlcs = 50, fundingPubKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1)) :+ 1.toByte).publicKey, @@ -364,27 +380,27 @@ object ChannelCodecsSpec { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000), Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000000), Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(2000000), Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, MilliSatoshi(3000000), Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(4000000), Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) ) val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingAmount = fundingTx.txOut(0).amount val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) - val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000, 70000000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) - val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000, 700000), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) + val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, MilliSatoshi(50000000), MilliSatoshi(70000000)), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) + val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, MilliSatoshi(50000), MilliSatoshi(700000)), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) val commitments = Commitments(ChannelVersion.STANDARD, localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, - originChannels = Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000L, 10000000L)), + originChannels = Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, MilliSatoshi(11000000L), MilliSatoshi(10000000L))), remoteNextCommitInfo = Right(randomKey.publicKey), commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = ByteVector32.Zeroes) - val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), 42, 15, 575, 53, Channel.MAX_FUNDING_SATOSHIS * 1000L) + val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), 42, MilliSatoshi(15), MilliSatoshi(575), 53, Channel.MAX_FUNDING.toMilliSatoshi) val normal = DATA_NORMAL(commitments, ShortChannelId(42), true, None, channelUpdate, None, None) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index 3b3ecd7a61..7f1f2ef22f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.crypto.Hmac256 -import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomBytes64} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, randomBytes32, randomBytes64} import fr.acinq.eclair.wire.FailureMessageCodecs._ import org.scalatest.FunSuite import scodec.bits._ @@ -36,8 +36,8 @@ class FailureMessageCodecsSpec extends FunSuite { cltvExpiryDelta = 100, messageFlags = 0, channelFlags = 1, - htlcMinimumMsat = 1000, - feeBaseMsat = 12, + htlcMinimumMsat = MilliSatoshi(1000), + feeBaseMsat = MilliSatoshi(12), feeProportionalMillionths = 76, htlcMaximumMsat = None) @@ -46,8 +46,8 @@ class FailureMessageCodecsSpec extends FunSuite { InvalidRealm :: TemporaryNodeFailure :: PermanentNodeFailure :: RequiredNodeFeatureMissing :: InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnionPayload(randomBytes32) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: - AmountBelowMinimum(123456, channelUpdate) :: FeeInsufficient(546463, channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: - IncorrectOrUnknownPaymentDetails(123456L) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil + AmountBelowMinimum(MilliSatoshi(123456), channelUpdate) :: FeeInsufficient(MilliSatoshi(546463), channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: + IncorrectOrUnknownPaymentDetails(MilliSatoshi(123456L)) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil msgs.foreach { msg => { @@ -75,7 +75,7 @@ class FailureMessageCodecsSpec extends FunSuite { val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) val testCases = Map( InvalidOnionKey(ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) -> hex"41a824e2d630111669fa3e52b600a518f369691909b4e89205dc624ee17ed2c1 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - IncorrectOrUnknownPaymentDetails(42) -> hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + IncorrectOrUnknownPaymentDetails(MilliSatoshi(42)) -> hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) for ((expected, bin) <- testCases) { @@ -112,7 +112,7 @@ class FailureMessageCodecsSpec extends FunSuite { test("support encoding of channel_update with/without type in failure messages") { val tmp_channel_failure_notype = hex"10070080cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val tmp_channel_failure_withtype = hex"100700820102cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" - val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, 0, 3, 14, 1000, 1, 1, None)) + val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, 0, 3, 14, MilliSatoshi(1000), MilliSatoshi(1), 1, None)) val u = failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value assert(u === ref) 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 fc033ae938..3649943675 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 @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.api._ import fr.acinq.eclair.channel.State @@ -58,15 +58,15 @@ class LightningMessageCodecsSpec extends FunSuite { } test("encode/decode all channel messages") { - val open = OpenChannel(randomBytes32, randomBytes32, 3, 4, 5, UInt64(6), 7, 8, 9, 10, 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) - val accept = AcceptChannel(randomBytes32, 3, UInt64(4), 5, 6, 7, 8, 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)) + val open = OpenChannel(randomBytes32, randomBytes32, Satoshi(3), MilliSatoshi(4), Satoshi(5), UInt64(6), Satoshi(7), MilliSatoshi(8), 9, 10, 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) + val accept = AcceptChannel(randomBytes32, Satoshi(3), UInt64(4), Satoshi(5), MilliSatoshi(6), 7, 8, 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)) val funding_created = FundingCreated(randomBytes32, bin32(0), 3, randomBytes64) val funding_signed = FundingSigned(randomBytes32, randomBytes64) val funding_locked = FundingLocked(randomBytes32, point(2)) val update_fee = UpdateFee(randomBytes32, 2) val shutdown = Shutdown(randomBytes32, bin(47, 0)) - val closing_signed = ClosingSigned(randomBytes32, 2, randomBytes64) - val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, TestConstants.emptyOnionPacket) + val closing_signed = ClosingSigned(randomBytes32, Satoshi(2), randomBytes64) + val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, MilliSatoshi(3), bin32(0), 4, TestConstants.emptyOnionPacket) val update_fulfill_htlc = UpdateFulfillHtlc(randomBytes32, 2, bin32(0)) val update_fail_htlc = UpdateFailHtlc(randomBytes32, 2, bin(154, 0)) val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32, 2, randomBytes32, 1111) @@ -74,7 +74,7 @@ class LightningMessageCodecsSpec extends FunSuite { val revoke_and_ack = RevokeAndAck(randomBytes32, scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomBytes64, randomBytes64, randomBytes64, randomBytes64, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) val node_announcement = NodeAnnouncement(randomBytes64, bin(1, 2), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) - val channel_update = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, 4, 5, 6, None) + val channel_update = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, MilliSatoshi(4), MilliSatoshi(5), 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32, ShortChannelId(42), randomBytes64, randomBytes64) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None) @@ -190,7 +190,7 @@ class LightningMessageCodecsSpec extends FunSuite { // this was generated by c-lightning val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00" val update = lightningMessageCodec.decode(bin.bits).require.value.asInstanceOf[ChannelUpdate] - assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129, 1, 1, 6, 1, 1, 10, Some(980000000L))) + assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129, 1, 1, 6, MilliSatoshi(1), MilliSatoshi(1), 10, Some(MilliSatoshi(980000000L)))) val nodeId = PublicKey(hex"03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") assert(Announcements.checkSig(update, nodeId)) val bin2 = ByteVector(lightningMessageCodec.encode(update).require.toByteArray) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala index 763a9ce42a..bf236ed7b7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.wire.OnionCodecs._ import org.scalatest.FunSuite import scodec.bits.HexStringSyntax @@ -40,7 +40,7 @@ class OnionCodecsSpec extends FunSuite { } test("encode/decode per-hop payload") { - val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) + val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = MilliSatoshi(142000), outgoingCltvValue = 500000) val bin = perHopPayloadCodec.encode(payload).require assert(bin.toByteVector.size === 33) val payload1 = perHopPayloadCodec.decode(bin).require.value diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala index ed96e755f0..1f955e9d4b 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala @@ -21,7 +21,7 @@ import java.time.LocalDateTime import akka.actor.{Actor, ActorLogging, ActorRef, Terminated} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin._ -import fr.acinq.eclair.CoinUtils +import fr.acinq.eclair.{CoinUtils, MilliSatoshi} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDisconnected} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumDisconnected, ElectrumReady} import fr.acinq.eclair.channel._ @@ -187,11 +187,11 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging val c = mainController.networkChannelsMap.get(channelUpdate.shortChannelId) if (Announcements.isNode1(channelUpdate.channelFlags)) { c.isNode1Enabled = Some(Announcements.isEnabled(channelUpdate.channelFlags)) - c.feeBaseMsatNode1_opt = Some(channelUpdate.feeBaseMsat) + c.feeBaseMsatNode1_opt = Some(channelUpdate.feeBaseMsat.toLong) c.feeProportionalMillionthsNode1_opt = Some(channelUpdate.feeProportionalMillionths) } else { c.isNode2Enabled = Some(Announcements.isEnabled(channelUpdate.channelFlags)) - c.feeBaseMsatNode2_opt = Some(channelUpdate.feeBaseMsat) + c.feeBaseMsatNode2_opt = Some(channelUpdate.feeBaseMsat.toLong) c.feeProportionalMillionthsNode2_opt = Some(channelUpdate.feeProportionalMillionths) } mainController.networkChannelsMap.put(channelUpdate.shortChannelId, c) @@ -200,7 +200,7 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging } case p: PaymentSucceeded => - val message = CoinUtils.formatAmountInUnit(MilliSatoshi(p.amountMsat), FxApp.getUnit, withUnit = true) + val message = CoinUtils.formatAmountInUnit(p.amount, FxApp.getUnit, withUnit = true) mainController.handlers.notification("Payment Sent", message, NOTIFICATION_SUCCESS) case p: PaymentFailed => diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala index 80e286051b..54765f834a 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala @@ -20,7 +20,7 @@ import java.util.UUID import akka.pattern.{AskTimeoutException, ask} import akka.util.Timeout -import fr.acinq.bitcoin.MilliSatoshi +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair._ import fr.acinq.eclair.gui.controllers._ import fr.acinq.eclair.io.{NodeURI, Peer} @@ -88,8 +88,8 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte (for { kit <- fKit sendPayment = req.minFinalCltvExpiry match { - case None => SendPayment(amountMsat, req.paymentHash, req.nodeId, req.routingInfo, maxAttempts = kit.nodeParams.maxPaymentAttempts) - case Some(minFinalCltvExpiry) => SendPayment(amountMsat, req.paymentHash, req.nodeId, req.routingInfo, finalCltvExpiry = minFinalCltvExpiry, maxAttempts = kit.nodeParams.maxPaymentAttempts) + case None => SendPayment(MilliSatoshi(amountMsat), req.paymentHash, req.nodeId, req.routingInfo, maxAttempts = kit.nodeParams.maxPaymentAttempts) + case Some(minFinalCltvExpiry) => SendPayment(MilliSatoshi(amountMsat), req.paymentHash, req.nodeId, req.routingInfo, finalCltvExpiry = minFinalCltvExpiry, maxAttempts = kit.nodeParams.maxPaymentAttempts) } res <- (kit.paymentInitiator ? sendPayment).mapTo[UUID] } yield res).recover { diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala index 13e9be2d83..c4691f0148 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.gui.controllers import akka.actor.ActorRef import com.google.common.base.Strings -import fr.acinq.bitcoin.MilliSatoshi +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.CoinUtils import fr.acinq.eclair.channel.{CMD_CLOSE, CMD_FORCECLOSE, Commitments} import fr.acinq.eclair.gui.FxApp @@ -129,12 +129,12 @@ class ChannelPaneController(val channelRef: ActorRef, val peerNodeId: String) ex } def updateBalance(commitments: Commitments) { - balance = MilliSatoshi(commitments.localCommit.spec.toLocalMsat) - capacity = MilliSatoshi(commitments.localCommit.spec.totalFunds) + balance = commitments.localCommit.spec.toLocal + capacity = commitments.localCommit.spec.totalFunds } def refreshBalance(): Unit = { - amountUs.setText(s"${CoinUtils.formatAmountInUnit(balance, FxApp.getUnit)} / ${CoinUtils.formatAmountInUnit(capacity, FxApp.getUnit, withUnit = true)}") + amountUs.setText(s"${CoinUtils.formatAmountInUnit(balance, FxApp.getUnit, false)} / ${CoinUtils.formatAmountInUnit(capacity, FxApp.getUnit, withUnit = true)}") balanceBar.setProgress(balance.amount.toDouble / capacity.amount) } diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala index 1a8707be1c..60626148c8 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala @@ -23,14 +23,14 @@ import java.util.Locale import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} import fr.acinq.eclair.gui.stages._ import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction, IndexedObservableList} import fr.acinq.eclair.gui.{FxApp, Handlers} import fr.acinq.eclair.payment.{PaymentEvent, PaymentReceived, PaymentRelayed, PaymentSent} import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement} -import fr.acinq.eclair.{CoinUtils, Setup, ShortChannelId} +import fr.acinq.eclair.{CoinUtils, MilliSatoshi, Setup, ShortChannelId} import grizzled.slf4j.Logging import javafx.animation.{FadeTransition, ParallelTransition, SequentialTransition, TranslateTransition} import javafx.application.{HostServices, Platform} diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala index 9f6560553e..361ba9337a 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala @@ -19,12 +19,13 @@ package fr.acinq.eclair.gui.controllers import java.lang.Boolean import com.google.common.base.Strings -import fr.acinq.bitcoin.{Satoshi, _} +import fr.acinq.bitcoin.Satoshi +import fr.acinq.eclair._ import fr.acinq.eclair.channel.{Channel, ChannelFlags} import fr.acinq.eclair.gui.utils.Constants import fr.acinq.eclair.gui.{FxApp, Handlers} import fr.acinq.eclair.io.{NodeURI, Peer} -import fr.acinq.eclair.{CoinUtils, Globals} +import fr.acinq.eclair.{CoinUtils, Globals, MilliSatoshi} import grizzled.slf4j.Logging import javafx.beans.value.{ChangeListener, ObservableValue} import javafx.event.ActionEvent @@ -79,8 +80,8 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo fundingSatError.setText("Capacity must be greater than 0") case Success(capacitySat) if capacitySat.amount < 50000 => fundingSatError.setText("Capacity is low and the channel may not be able to open") - case Success(capacitySat) if capacitySat.amount >= Channel.MAX_FUNDING_SATOSHIS => - fundingSatError.setText(s"Capacity must be less than ${CoinUtils.formatAmountInUnit(Satoshi(Channel.MAX_FUNDING_SATOSHIS), FxApp.getUnit, withUnit = true)}") + case Success(capacitySat) if capacitySat >= Channel.MAX_FUNDING => + fundingSatError.setText(s"Capacity must be less than ${CoinUtils.formatAmountInUnit(Channel.MAX_FUNDING, FxApp.getUnit, withUnit = true)}") case Success(_) => fundingSatError.setText("") case _ => fundingSatError.setText("Capacity is not valid") } @@ -100,9 +101,9 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo Try(if (Strings.isNullOrEmpty(feerateField.getText())) None else Some(feerateField.getText().toLong))) match { case (Success(capacitySat), _, _) if capacitySat.amount <= 0 => fundingSatError.setText("Capacity must be greater than 0") - case (Success(capacitySat), _, _) if capacitySat.amount >= Channel.MAX_FUNDING_SATOSHIS => - fundingSatError.setText(s"Capacity must be less than ${CoinUtils.formatAmountInUnit(Satoshi(Channel.MAX_FUNDING_SATOSHIS), FxApp.getUnit, withUnit = true)}") - case (Success(capacitySat), Success(pushMsat), _) if pushMsat > satoshi2millisatoshi(capacitySat).amount => + case (Success(capacitySat), _, _) if capacitySat >= Channel.MAX_FUNDING => + fundingSatError.setText(s"Capacity must be less than ${CoinUtils.formatAmountInUnit(Channel.MAX_FUNDING, FxApp.getUnit, withUnit = true)}") + case (Success(capacitySat), Success(pushMsat), _) if pushMsat > capacitySat.toMilliSatoshi.toLong => pushMsatError.setText("Push must be less or equal to capacity") case (Success(_), Success(pushMsat), _) if pushMsat < 0 => pushMsatError.setText("Push must be positive") diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala index b0ad2fe916..2abe281afb 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ReceivePaymentController.scala @@ -24,7 +24,7 @@ import javafx.scene.image.{ImageView, WritableImage} import javafx.scene.layout.GridPane import javafx.stage.Stage -import fr.acinq.bitcoin.MilliSatoshi +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.CoinUtils import fr.acinq.eclair.gui.{FxApp, Handlers} import fr.acinq.eclair.gui.utils._ diff --git a/pom.xml b/pom.xml index ed1e45b218..e5aa8bbc1d 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 2.4.20 10.0.11 1.3.9 - 0.13 + 0.14 24.0-android From 0780fc2216aba1012b8936e55a080c871d83b3c8 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Thu, 22 Aug 2019 18:58:13 +0200 Subject: [PATCH 65/86] Extended Queries: use TLV format for optional data (#1072) * Extended Queries: use TLV format for optional data Optional query extensions now use TLV instead of a custom format. Flags are encoded as varint instead of bytes as originally proposed. With the current proposal they will all fit on a single byte, but will be much easier to extends this way. * Move query message TLVs to their own namespace We add one new class for each TLV type, with specific TLV types, and encapsulate codecs. * Optional TLVs are represented as a list, not an optional list TLVs that extend regular LN messages can be represented as a TlvStream and not an Option[TlvStream] since we don't need to explicitely terminate the stream (either by preprending its length or using a specific terminator) as we do in Onion TLVs. No TLVs simply means that the TLV stream is empty. * Update to match BOLT PR Checksums in ReplyChannelRange now have the same encoding as short channel ids and timestamps: one byte for the encoding type (uncompressed or zlib) followed by encoded data. * TLV Stream: Implement a generic "get" method for TLV fields If a have a TLV stream of type MyTLV which is a subtype of TLV, and MyTLV1 and MYTLV2 are both subtypes of MyTLV then we can use stream.get[MyTLV1] to get the TLV record of type MYTLV1 (if any) in our TLV stream. * Extended range queries: Implement latest BOLT changes Checksums are just transmitted as a raw array, with optional compression as it would be useless here. * Use extended range queries on regtest and testnet We will use them on mainnet as soon as https://github.com/lightningnetwork/lightning-rfc/pull/557 has been merged. * Address review comments * Router: rework handling of ReplyChannelRange We remove the ugly and inefficient zipWithIndex we had before * NodeParams: move fee base check to its proper place * Router: minor cleanup --- .../scala/fr/acinq/eclair/NodeParams.scala | 9 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 13 +- .../scala/fr/acinq/eclair/router/Router.scala | 127 +++++++++++------- .../fr/acinq/eclair/wire/CommonCodecs.scala | 6 +- .../eclair/wire/LightningMessageCodecs.scala | 70 ++++------ .../eclair/wire/LightningMessageTypes.scala | 61 ++++----- .../eclair/wire/QueryChannelRangeTlv.scala | 37 +++++ .../eclair/wire/QueryShortChannelIdsTlv.scala | 41 ++++++ .../eclair/wire/ReplyChannelRangeTlv.scala | 64 +++++++++ .../scala/fr/acinq/eclair/wire/TlvTypes.scala | 13 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 12 +- .../router/ChannelRangeQueriesSpec.scala | 35 ++--- .../acinq/eclair/router/RoutingSyncSpec.scala | 8 +- .../wire/ExtendedQueriesCodecsSpec.scala | 94 +++++++++++++ .../wire/LightningMessageCodecsSpec.scala | 104 +++++++++----- .../fr/acinq/eclair/wire/TlvCodecsSpec.scala | 6 + 16 files changed, 509 insertions(+), 191 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryChannelRangeTlv.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/ReplyChannelRangeTlv.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala 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 2daa66510f..093d3b8a64 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -33,6 +33,7 @@ import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector + import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -76,7 +77,6 @@ case class NodeParams(keyManager: KeyManager, routerConf: RouterConf, socksProxy_opt: Option[Socks5ProxyParams], maxPaymentAttempts: Int) { - val privateKey = keyManager.nodeKey.privateKey val nodeId = keyManager.nodeId } @@ -186,6 +186,11 @@ object NodeParams { claimMainBlockTarget = config.getInt("on-chain-fees.target-blocks.claim-main") ) + val feeBase = MilliSatoshi(config.getInt("fee-base-msat")) + // fee base is in msat but is encoded on 32 bits and not 64 in the BOLTs, which is why it has + // to be below 0x100000000 msat which is about 42 mbtc + require(feeBase <= MilliSatoshi(0xFFFFFFFFL), "fee-base-msat must be below 42 mbtc") + NodeParams( keyManager = keyManager, alias = nodeAlias, @@ -209,7 +214,7 @@ object NodeParams { toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"), maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"), minDepthBlocks = config.getInt("mindepth-blocks"), - feeBase = MilliSatoshi(config.getInt("fee-base-msat")), + feeBase = feeBase, feeProportionalMillionth = config.getInt("fee-proportional-millionths"), reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"), 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 32c4d52cf2..6f76f5dfa6 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 @@ -25,8 +25,7 @@ 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, DeterministicWallet, Protocol, Satoshi} -import fr.acinq.eclair +import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet, Protocol, Satoshi} import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler @@ -155,7 +154,15 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter // NB: we always add extended info; if peer doesn't understand them it will ignore them - router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) + + // README: for now we do not activate extended queries on mainnet + val flags_opt = nodeParams.chainHash match { + case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => + log.info("using extended range queries") + Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) + case _ => None + } + router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = flags_opt) } // let's bring existing/requested channels online 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 c6bf7cdb8b..3d80cba7e0 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.router -import java.util.zip.Adler32 +import java.util.zip.CRC32 import akka.Done import akka.actor.{ActorRef, Props, Status} @@ -35,9 +35,9 @@ import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{RichWeight, WeightRatios} import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ - import shapeless.HNil +import scala.annotation.tailrec import scala.collection.immutable.{SortedMap, TreeMap} import scala.collection.{SortedSet, mutable} import scala.compat.Platform @@ -76,13 +76,13 @@ case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChan } case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed) case class LiftChannelExclusion(desc: ChannelDesc) -case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef, flags_opt: Option[ExtendedQueryFlags]) +case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef, flags_opt: Option[QueryChannelRangeTlv]) case object GetRoutingState case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) -case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Byte) +case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Long) case class Sync(pending: List[RoutingMessage], total: Int) @@ -431,7 +431,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ // ask for everything // we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't // have to worry about sending a new query_channel_range when another query is still in progress - val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue, extendedQueryFlags_opt = flags_opt) + val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0L, numberOfBlocks = Int.MaxValue.toLong, TlvStream(flags_opt.toList)) log.info("sending query_channel_range={}", query) remote ! query @@ -508,37 +508,62 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ val shortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, firstBlockNum, numberOfBlocks) split(shortChannelIds) - .foreach(chunk => - transport ! ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, + .foreach(chunk => { + val (timestamps, checksums) = routingMessage.queryFlags_opt match { + case Some(extension) if extension.wantChecksums | extension.wantTimestamps => + // we always compute timestamps and checksums even if we don't need both, overhead is negligible + val (timestamps, checksums) = chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates)).unzip + val encodedTimestamps = if (extension.wantTimestamps) Some(ReplyChannelRangeTlv.EncodedTimestamps(EncodingType.UNCOMPRESSED, timestamps)) else None + val encodedChecksums = if (extension.wantChecksums) Some(ReplyChannelRangeTlv.EncodedChecksums(checksums)) else None + (encodedTimestamps, encodedChecksums) + case _ => (None, None) + } + val reply = ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, shortChannelIds = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, chunk.shortChannelIds), - extendedQueryFlags_opt = extendedQueryFlags_opt, - extendedInfo_opt = extendedQueryFlags_opt map { - case ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS => ExtendedInfo(chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates))) - })) + timestamps = timestamps, + checksums = checksums) + transport ! reply + }) stay - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, shortChannelIds, extendedQueryFlags_opt, extendedInfo_opt)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, shortChannelIds, _)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val shortChannelIdAndFlags = shortChannelIds.array - .zipWithIndex - .map { case (shortChannelId: ShortChannelId, idx) => ShortChannelIdAndFlag(shortChannelId, computeFlag(d.channels, d.updates)(shortChannelId, extendedInfo_opt.map(_.array(idx)))) } - .filter(_.flag != 0) + + @tailrec + def loop(ids: List[ShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = { + ids match { + case Nil => acc.reverse + case head :: tail => + val flag = computeFlag(d.channels, d.updates)(head, timestamps.headOption, checksums.headOption) + // 0 means nothing to query, just don't include it + val acc1 = if (flag != 0) ShortChannelIdAndFlag(head, flag) :: acc else acc + loop(tail, timestamps.drop(1), checksums.drop(1), acc1) + } + } + + val timestamps_opt = routingMessage.timestamps_opt.map(_.timestamps).getOrElse(List.empty[ReplyChannelRangeTlv.Timestamps]) + val checksums_opt = routingMessage.checksums_opt.map(_.checksums).getOrElse(List.empty[ReplyChannelRangeTlv.Checksums]) + + val shortChannelIdAndFlags = loop(shortChannelIds.array, timestamps_opt, checksums_opt) + val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) { case ((c, u), ShortChannelIdAndFlag(_, flag)) => - val c1 = c + (if (QueryFlagTypes.includeAnnouncement(flag)) 1 else 0) - val u1 = u + (if (QueryFlagTypes.includeUpdate1(flag)) 1 else 0) + (if (QueryFlagTypes.includeUpdate2(flag)) 1 else 0) + val c1 = c + (if (QueryShortChannelIdsTlv.QueryFlagType.includeAnnouncement(flag)) 1 else 0) + val u1 = u + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate1(flag)) 1 else 0) + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate2(flag)) 1 else 0) (c1, u1) } - log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={} queryFlags=${extendedQueryFlags_opt.getOrElse("n/a")}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) + log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) val replies = shortChannelIdAndFlags .grouped(SHORTID_WINDOW) .map(chunk => QueryShortChannelIds(chainHash, shortChannelIds = EncodedShortChannelIds(shortChannelIds.encoding, chunk.map(_.shortChannelId)), - queryFlags_opt = extendedQueryFlags_opt map { - case _ => EncodedQueryFlags(shortChannelIds.encoding, chunk.map(_.flag)) - })) + if (routingMessage.timestamps_opt.isDefined || routingMessage.checksums_opt.isDefined) + TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(shortChannelIds.encoding, chunk.map(_.flag))) + else + TlvStream.empty + )) .toList val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) // we only send a rely right away if there were no pending requests @@ -554,16 +579,16 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ case ((c, u), (shortChannelId, idx)) => var c1 = c var u1 = u - val flag = queryFlags_opt.map(_.array(idx)).getOrElse(QueryFlagTypes.INCLUDE_ALL) + val flag = routingMessage.queryFlags_opt.map(_.array(idx)).getOrElse(QueryShortChannelIdsTlv.QueryFlagType.INCLUDE_ALL) d.channels.get(shortChannelId) match { case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) case Some(ca) => - if (QueryFlagTypes.includeAnnouncement(flag)) { + if (QueryShortChannelIdsTlv.QueryFlagType.includeAnnouncement(flag)) { transport ! ca c1 = c1 + 1 } - if (QueryFlagTypes.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } - if (QueryFlagTypes.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } + if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } + if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } } (c1, u1) } @@ -721,7 +746,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ // when we're sending updates to ourselves (transport_opt, remoteNodeId_opt) match { case (Some(transport), Some(remoteNodeId)) => - val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(u.shortChannelId)), queryFlags_opt = None) + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(u.shortChannelId)), TlvStream.empty) d.sync.get(remoteNodeId) match { case Some(sync) => // we already have a pending request to that node, let's add this channel to the list and we'll get it later @@ -828,25 +853,41 @@ object Router { height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } - def computeFlag(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId, theirInfo_opt: Option[TimestampsAndChecksums]): Byte = { - var flag = 0 - theirInfo_opt match { - case Some(theirInfo) if channels.contains(shortChannelId) => - val ourInfo = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + def computeFlag(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])( + shortChannelId: ShortChannelId, + timestamps_opt: Option[ReplyChannelRangeTlv.Timestamps], + checksums_opt: Option[ReplyChannelRangeTlv.Checksums]): Long = { + import QueryShortChannelIdsTlv.QueryFlagType + var flag = 0L + (timestamps_opt, checksums_opt) match { + case (Some(theirTimestamps), Some(theirChecksums)) if channels.contains(shortChannelId) => + val (ourTimestamps, ourChecksums) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) // we request their channel_update if all those conditions are met: // - it is more recent than ours // - it is different from ours, or it is the same but ours is about to be stale // - it is not stale itself - if (ourInfo.timestamp1 < theirInfo.timestamp1 && (ourInfo.checksum1 != theirInfo.checksum1 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp1)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 - if (ourInfo.timestamp2 < theirInfo.timestamp2 && (ourInfo.checksum2 != theirInfo.checksum2 || isAlmostStale(ourInfo.timestamp1)) && !isStale(theirInfo.timestamp2)) flag = flag | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 - case None if channels.contains(shortChannelId) => + if (ourTimestamps.timestamp1 < theirTimestamps.timestamp1 && (ourChecksums.checksum1 != theirChecksums.checksum1 || isAlmostStale(ourTimestamps.timestamp1)) && !isStale(theirTimestamps.timestamp1)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 + if (ourTimestamps.timestamp2 < theirTimestamps.timestamp2 && (ourChecksums.checksum2 != theirChecksums.checksum2 || isAlmostStale(ourTimestamps.timestamp1)) && !isStale(theirTimestamps.timestamp2)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 + case (Some(theirTimestamps), None) if channels.contains(shortChannelId) => + val (ourTimestamps, _) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + // we request their channel_update if all those conditions are met: + // - it is more recent than ours + // - it is not stale itself + if (ourTimestamps.timestamp1 < theirTimestamps.timestamp1 && !isStale(theirTimestamps.timestamp1)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 + if (ourTimestamps.timestamp2 < theirTimestamps.timestamp2 && !isStale(theirTimestamps.timestamp2)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 + case (None, Some(theirChecksums)) if channels.contains(shortChannelId) => + val (_, ourChecksums) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + // this should not happen as we will not ask for checksums without asking for timestamps too + if (ourChecksums.checksum1 != theirChecksums.checksum1 && theirChecksums.checksum1 != 0) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 + if (ourChecksums.checksum2 != theirChecksums.checksum2 && theirChecksums.checksum2 != 0) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 + case (None, None) if channels.contains(shortChannelId) => // we know this channel: we only request their channel updates - flag = QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + flag = QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 case _ => // we don't know this channel: we request everything - flag = QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2 + flag = QueryFlagType.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 } - flag.toByte + flag } /** @@ -900,7 +941,7 @@ object Router { timestamp } - def getChannelDigestInfo(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId): TimestampsAndChecksums = { + def getChannelDigestInfo(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(shortChannelId: ShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { val c = channels(shortChannelId) val u1_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) val u2_opt = updates.get(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) @@ -908,17 +949,13 @@ object Router { val timestamp2 = u2_opt.map(_.timestamp).getOrElse(0L) val checksum1 = u1_opt.map(getChecksum).getOrElse(0L) val checksum2 = u2_opt.map(getChecksum).getOrElse(0L) - TimestampsAndChecksums( - timestamp1 = timestamp1, - checksum1 = checksum1, - timestamp2 = timestamp2, - checksum2 = checksum2) + (ReplyChannelRangeTlv.Timestamps(timestamp1 = timestamp1, timestamp2 = timestamp2), ReplyChannelRangeTlv.Checksums(checksum1 = checksum1, checksum2 = checksum2)) } def getChecksum(u: ChannelUpdate): Long = { import u._ val data = serializationResult(LightningMessageCodecs.channelUpdateChecksumCodec.encode(shortChannelId :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: HNil)) - val checksum = new Adler32() + val checksum = new CRC32() checksum.update(data.toArray) checksum.getValue } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala index fc749c8fe8..b0cb2da720 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.crypto.Mac32 import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} @@ -57,6 +57,10 @@ object CommonCodecs { val satoshi: Codec[Satoshi] = uint64overflow.xmapc(l => Satoshi(l))(_.toLong) val millisatoshi: Codec[MilliSatoshi] = uint64overflow.xmapc(l => MilliSatoshi(l))(_.amount) + // this is needed because some millisatoshi values are encoded on 32 bits in the BOLTs + // this codec will fail if the amount does not fit on 32 bits + val millisatoshi32: Codec[MilliSatoshi] = uint32.xmapc(l => MilliSatoshi(l))(_.amount) + /** * We impose a minimal encoding on some values (such as varint and truncated int) to ensure that signed hashes can be * re-computed correctly. 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 0f185fbcb9..9c753925f3 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,8 +16,8 @@ package fr.acinq.eclair.wire +import fr.acinq.eclair.wire import fr.acinq.eclair.wire.CommonCodecs._ -import fr.acinq.eclair.{MilliSatoshi, wire} import scodec.Codec import scodec.codecs._ @@ -188,12 +188,11 @@ object LightningMessageCodecs { ("channelFlags" | byte) :: ("cltvExpiryDelta" | uint16) :: ("htlcMinimumMsat" | millisatoshi) :: - ("feeBaseMsat" | uint32.xmapc(l => MilliSatoshi(l))(_.amount)) :: + ("feeBaseMsat" | millisatoshi32) :: ("feeProportionalMillionths" | uint32) :: ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, millisatoshi)) }) - val channelUpdateWitnessCodec = ("chainHash" | bytes32) :: ("shortChannelId" | shortchannelid) :: @@ -202,7 +201,7 @@ object LightningMessageCodecs { ("channelFlags" | byte) :: ("cltvExpiryDelta" | uint16) :: ("htlcMinimumMsat" | millisatoshi) :: - ("feeBaseMsat" | uint32.xmapc(l => MilliSatoshi(l))(_.amount)) :: + ("feeBaseMsat" | millisatoshi32) :: ("feeProportionalMillionths" | uint32) :: ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, millisatoshi)) :: ("unknownFields" | bytes) @@ -217,51 +216,38 @@ object LightningMessageCodecs { .\(0) { case a@EncodedShortChannelIds(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) .\(1) { case a@EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) - val encodedQueryFlagsCodec: Codec[EncodedQueryFlags] = - discriminated[EncodedQueryFlags].by(byte) - .\(0) { case a@EncodedQueryFlags(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(byte)).as[EncodedQueryFlags]) - .\(1) { case a@EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(byte))).as[EncodedQueryFlags]) - - val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( - ("chainHash" | bytes32) :: - ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: - ("queryFlags_opt" | optional(bitsRemaining, variableSizeBytes(uint16, encodedQueryFlagsCodec))) + val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = { + Codec( + ("chainHash" | bytes32) :: + ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: + ("tlvStream" | QueryShortChannelIdsTlv.codec) ).as[QueryShortChannelIds] + } val replyShortChanelIdsEndCodec: Codec[ReplyShortChannelIdsEnd] = ( ("chainHash" | bytes32) :: ("complete" | byte) ).as[ReplyShortChannelIdsEnd] - val extendedQueryFlagsCodec: Codec[ExtendedQueryFlags] = - discriminated[ExtendedQueryFlags].by(byte) - .typecase(1, provide(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) - - val queryChannelRangeCodec: Codec[QueryChannelRange] = ( - ("chainHash" | bytes32) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) :: - ("optionExtendedQueryFlags" | optional(bitsRemaining, extendedQueryFlagsCodec)) - ).as[QueryChannelRange] - - val timestampsAndChecksumsCodec: Codec[TimestampsAndChecksums] = ( - ("timestamp1" | uint32) :: - ("timestamp2" | uint32) :: - ("checksum1" | uint32) :: - ("checksum2" | uint32) - ).as[TimestampsAndChecksums] - - val extendedInfoCodec: Codec[ExtendedInfo] = list(timestampsAndChecksumsCodec).as[ExtendedInfo] - - val replyChannelRangeCodec: Codec[ReplyChannelRange] = ( - ("chainHash" | bytes32) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) :: - ("complete" | byte) :: - ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: - ("optionExtendedQueryFlags_opt" | optional(bitsRemaining, extendedQueryFlagsCodec)) :: - ("extendedInfo_opt" | optional(bitsRemaining, variableSizeBytes(uint16, extendedInfoCodec))) - ).as[ReplyChannelRange] + val queryChannelRangeCodec: Codec[QueryChannelRange] = { + Codec( + ("chainHash" | bytes32) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("tlvStream" | QueryChannelRangeTlv.codec) + ).as[QueryChannelRange] + } + + val replyChannelRangeCodec: Codec[ReplyChannelRange] = { + Codec( + ("chainHash" | bytes32) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("complete" | byte) :: + ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: + ("tlvStream" | ReplyChannelRangeTlv.codec) + ).as[ReplyChannelRange] + } val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( ("chainHash" | 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 b84e5e3972..a172c3d2e5 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 @@ -20,8 +20,8 @@ import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import java.nio.charset.StandardCharsets import com.google.common.base.Charsets -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -233,61 +233,54 @@ object EncodingType { } // @formatter:on -case object QueryFlagTypes { - val INCLUDE_CHANNEL_ANNOUNCEMENT: Byte = 1 - val INCLUDE_CHANNEL_UPDATE_1: Byte = 2 - val INCLUDE_CHANNEL_UPDATE_2: Byte = 4 - val INCLUDE_ALL: Byte = (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte - - def includeAnnouncement(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_ANNOUNCEMENT) != 0 - - def includeUpdate1(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_1) != 0 - - def includeUpdate2(flag: Byte) = (flag & QueryFlagTypes.INCLUDE_CHANNEL_UPDATE_2) != 0 -} case class EncodedShortChannelIds(encoding: EncodingType, array: List[ShortChannelId]) -case class EncodedQueryFlags(encoding: EncodingType, - array: List[Byte]) case class QueryShortChannelIds(chainHash: ByteVector32, shortChannelIds: EncodedShortChannelIds, - queryFlags_opt: Option[EncodedQueryFlags]) extends RoutingMessage with HasChainHash + tlvStream: TlvStream[QueryShortChannelIdsTlv] = TlvStream.empty) extends RoutingMessage with HasChainHash { + val queryFlags_opt: Option[QueryShortChannelIdsTlv.EncodedQueryFlags] = tlvStream.get[QueryShortChannelIdsTlv.EncodedQueryFlags] +} case class ReplyShortChannelIdsEnd(chainHash: ByteVector32, complete: Byte) extends RoutingMessage with HasChainHash -// @formatter:off -sealed trait ExtendedQueryFlags -object ExtendedQueryFlags { - case object TIMESTAMPS_AND_CHECKSUMS extends ExtendedQueryFlags -} -// @formatter:on case class QueryChannelRange(chainHash: ByteVector32, firstBlockNum: Long, numberOfBlocks: Long, - extendedQueryFlags_opt: Option[ExtendedQueryFlags]) extends RoutingMessage with HasChainHash + tlvStream: TlvStream[QueryChannelRangeTlv] = TlvStream.empty) extends RoutingMessage { + val queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags] = tlvStream.get[QueryChannelRangeTlv.QueryFlags] +} case class ReplyChannelRange(chainHash: ByteVector32, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, shortChannelIds: EncodedShortChannelIds, - extendedQueryFlags_opt: Option[ExtendedQueryFlags], - extendedInfo_opt: Option[ExtendedInfo]) extends RoutingMessage with HasChainHash { - extendedInfo_opt.foreach(extendedInfo => require(shortChannelIds.array.size == extendedInfo.array.size, s"shortChannelIds.size=${shortChannelIds.array.size} != extendedInfo.size=${extendedInfo.array.size}")) + tlvStream: TlvStream[ReplyChannelRangeTlv] = TlvStream.empty) extends RoutingMessage { + val timestamps_opt: Option[ReplyChannelRangeTlv.EncodedTimestamps] = tlvStream.get[ReplyChannelRangeTlv.EncodedTimestamps] + + val checksums_opt: Option[ReplyChannelRangeTlv.EncodedChecksums] = tlvStream.get[ReplyChannelRangeTlv.EncodedChecksums] } -case class GossipTimestampFilter(chainHash: ByteVector32, - firstTimestamp: Long, - timestampRange: Long) extends RoutingMessage with HasChainHash +object ReplyChannelRange { + def apply(chainHash: ByteVector32, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + shortChannelIds: EncodedShortChannelIds, + timestamps: Option[ReplyChannelRangeTlv.EncodedTimestamps], + checksums: Option[ReplyChannelRangeTlv.EncodedChecksums]) = { + timestamps.foreach(ts => require(ts.timestamps.length == shortChannelIds.array.length)) + checksums.foreach(cs => require(cs.checksums.length == shortChannelIds.array.length)) + new ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, complete, shortChannelIds, TlvStream(timestamps.toList ::: checksums.toList)) + } +} -case class TimestampsAndChecksums(timestamp1: Long, - checksum1: Long, - timestamp2: Long, - checksum2: Long) -case class ExtendedInfo(array: List[TimestampsAndChecksums]) \ No newline at end of file +case class GossipTimestampFilter(chainHash: ByteVector32, + firstTimestamp: Long, + timestampRange: Long) extends RoutingMessage with HasChainHash \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryChannelRangeTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryChannelRangeTlv.scala new file mode 100644 index 0000000000..0dc5f57050 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryChannelRangeTlv.scala @@ -0,0 +1,37 @@ +package fr.acinq.eclair.wire + +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.wire.CommonCodecs.{shortchannelid, varint, varintoverflow} +import scodec.Codec +import scodec.codecs._ + +sealed trait QueryChannelRangeTlv extends Tlv + +object QueryChannelRangeTlv { + /** + * Optional query flag that is appended to QueryChannelRange + * @param flag bit 1 set means I want timestamps, bit 2 set means I want checksums + */ + case class QueryFlags(flag: Long) extends QueryChannelRangeTlv { + val wantTimestamps = QueryFlags.wantTimestamps(flag) + + val wantChecksums = QueryFlags.wantChecksums(flag) + } + + case object QueryFlags { + val WANT_TIMESTAMPS: Long = 1 + val WANT_CHECKSUMS: Long = 2 + val WANT_ALL: Long = (WANT_TIMESTAMPS | WANT_CHECKSUMS) + + def wantTimestamps(flag: Long) = (flag & WANT_TIMESTAMPS) != 0 + + def wantChecksums(flag: Long) = (flag & WANT_CHECKSUMS) != 0 + } + + val queryFlagsCodec: Codec[QueryFlags] = Codec(("flag" | varintoverflow)).as[QueryFlags] + + val codec: Codec[TlvStream[QueryChannelRangeTlv]] = TlvCodecs.tlvStream(discriminated.by(varint) + .typecase(UInt64(1), variableSizeBytesLong(varintoverflow, queryFlagsCodec)) + ) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala new file mode 100644 index 0000000000..3c878b2d7c --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala @@ -0,0 +1,41 @@ +package fr.acinq.eclair.wire + +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.wire.CommonCodecs.{shortchannelid, varint, varintoverflow} +import scodec.Codec +import scodec.codecs.{byte, discriminated, list, provide, variableSizeBytesLong, zlib} + +sealed trait QueryShortChannelIdsTlv extends Tlv + +object QueryShortChannelIdsTlv { + + /** + * Optional TLV-based query message that can be appended to QueryShortChannelIds + * @param encoding 0 means uncompressed, 1 means compressed with zlib + * @param array array of query flags, each flags specifies the info we want for a given channel + */ + case class EncodedQueryFlags(encoding: EncodingType, array: List[Long]) extends QueryShortChannelIdsTlv + + case object QueryFlagType { + val INCLUDE_CHANNEL_ANNOUNCEMENT: Long = 1 + val INCLUDE_CHANNEL_UPDATE_1: Long = 2 + val INCLUDE_CHANNEL_UPDATE_2: Long = 4 + val INCLUDE_ALL: Long = (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2) + + def includeAnnouncement(flag: Long) = (flag & INCLUDE_CHANNEL_ANNOUNCEMENT) != 0 + + def includeUpdate1(flag: Long) = (flag & INCLUDE_CHANNEL_UPDATE_1) != 0 + + def includeUpdate2(flag: Long) = (flag & INCLUDE_CHANNEL_UPDATE_2) != 0 + } + + val encodedQueryFlagsCodec: Codec[EncodedQueryFlags] = + discriminated[EncodedQueryFlags].by(byte) + .\(0) { case a@EncodedQueryFlags(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(varintoverflow)).as[EncodedQueryFlags]) + .\(1) { case a@EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(varintoverflow))).as[EncodedQueryFlags]) + + + val codec: Codec[TlvStream[QueryShortChannelIdsTlv]] = TlvCodecs.tlvStream(discriminated.by(varint) + .typecase(UInt64(1), variableSizeBytesLong(varintoverflow, encodedQueryFlagsCodec)) + ) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ReplyChannelRangeTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ReplyChannelRangeTlv.scala new file mode 100644 index 0000000000..bde4605551 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ReplyChannelRangeTlv.scala @@ -0,0 +1,64 @@ +package fr.acinq.eclair.wire + +import fr.acinq.eclair.{UInt64, wire} +import fr.acinq.eclair.wire.CommonCodecs.{varint, varintoverflow} +import scodec.Codec +import scodec.codecs._ + +sealed trait ReplyChannelRangeTlv extends Tlv + +object ReplyChannelRangeTlv { + + /** + * + * @param timestamp1 timestamp for node 1, or 0 + * @param timestamp2 timestamp for node 2, or 0 + */ + case class Timestamps(timestamp1: Long, timestamp2: Long) + + /** + * Optional timestamps TLV that can be appended to ReplyChannelRange + * + * @param encoding same convention as for short channel ids + * @param timestamps + */ + case class EncodedTimestamps(encoding: EncodingType, timestamps: List[Timestamps]) extends ReplyChannelRangeTlv + + /** + * + * @param checksum1 checksum for node 1, or 0 + * @param checksum2 checksum for node 2, or 0 + */ + case class Checksums(checksum1: Long, checksum2: Long) + + /** + * Optional checksums TLV that can be appended to ReplyChannelRange + * + * @param checksums + */ + case class EncodedChecksums(checksums: List[Checksums]) extends ReplyChannelRangeTlv + + val timestampsCodec: Codec[Timestamps] = ( + ("checksum1" | uint32) :: + ("checksum2" | uint32) + ).as[Timestamps] + + val encodedTimestampsCodec: Codec[EncodedTimestamps] = variableSizeBytesLong(varintoverflow, + discriminated[EncodedTimestamps].by(byte) + .\(0) { case a@EncodedTimestamps(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(timestampsCodec)).as[EncodedTimestamps]) + .\(1) { case a@EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(timestampsCodec))).as[EncodedTimestamps]) + ) + + val checksumsCodec: Codec[Checksums] = ( + ("checksum1" | uint32) :: + ("checksum2" | uint32) + ).as[Checksums] + + val encodedChecksumsCodec: Codec[EncodedChecksums] = variableSizeBytesLong(varintoverflow, list(checksumsCodec)).as[EncodedChecksums] + + val innerCodec = discriminated[ReplyChannelRangeTlv].by(varint) + .typecase(UInt64(1), encodedTimestampsCodec) + .typecase(UInt64(3), encodedChecksumsCodec) + + val codec: Codec[TlvStream[ReplyChannelRangeTlv]] = TlvCodecs.tlvStream(innerCodec) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala index e4eea124f8..b87b1ebcb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala @@ -19,6 +19,8 @@ package fr.acinq.eclair.wire import fr.acinq.eclair.UInt64 import scodec.bits.ByteVector +import scala.reflect.ClassTag + /** * Created by t-bast on 20/06/2019. */ @@ -45,9 +47,18 @@ case class GenericTlv(tag: UInt64, value: ByteVector) extends Tlv * @param unknown unknown tlv records. * @tparam T the stream namespace is a trait extending the top-level tlv trait. */ -case class TlvStream[T <: Tlv](records: Traversable[T], unknown: Traversable[GenericTlv] = Nil) +case class TlvStream[T <: Tlv](records: Traversable[T], unknown: Traversable[GenericTlv] = Nil) { + /** + * + * @tparam R input type parameter, must be a subtype of the main TLV type + * @return the TLV record of of type that matches the input type parameter if any (there can be at most one, since BOLTs specify + * that TLV records are supposed to be unique + */ + def get[R <: T : ClassTag]: Option[R] = records.collectFirst { case r: R => r } +} object TlvStream { + def empty[T <: Tlv] = TlvStream[T](Nil, Nil) def apply[T <: Tlv](records: T*): TlvStream[T] = TlvStream(records, Nil) 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 074169d83c..e9f0414147 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 @@ -26,14 +26,13 @@ import fr.acinq.bitcoin.{Satoshi} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} -import fr.acinq.eclair.channel.{ChannelCreated, HasCommitments} import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.channel.{ChannelCreated, HasCommitments} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer._ -import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong} -import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, randomBytes, wire, _} +import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec} +import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, Tlv, TlvStream} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -337,7 +336,10 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) - val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42000))), queryFlags_opt = None) + val query = QueryShortChannelIds( + Alice.nodeParams.chainHash, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42000))), + TlvStream.empty) // make sure that routing messages go through for (ann <- channels ++ updates) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 9962ba50dd..065ee73ac3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.router -import fr.acinq.eclair.{MilliSatoshi, randomKey} +import fr.acinq.eclair.wire.ReplyChannelRangeTlv._ import fr.acinq.eclair.wire._ +import fr.acinq.eclair.{MilliSatoshi, randomKey} import org.scalatest.FunSuite import scala.collection.immutable.SortedMap @@ -33,14 +34,14 @@ class ChannelRangeQueriesSpec extends FunSuite { val a = randomKey.publicKey val b = randomKey.publicKey val ab = RouteCalculationSpec.makeChannel(123466L, a, b) - val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, MilliSatoshi(0), 0, timestamp = now) - val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, MilliSatoshi(0), 0, timestamp = now) + val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, MilliSatoshi(0), 0, timestamp = now) + val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, MilliSatoshi(0), 0, timestamp = now) val c = randomKey.publicKey val d = randomKey.publicKey val cd = RouteCalculationSpec.makeChannel(451312L, c, d) - val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, MilliSatoshi(0), 0, timestamp = now) - val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, MilliSatoshi(0), 0, timestamp = now) + val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, MilliSatoshi(0), 0, timestamp = now) + val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, MilliSatoshi(0), 0, timestamp = now) val e = randomKey.publicKey val f = randomKey.publicKey @@ -57,28 +58,28 @@ class ChannelRangeQueriesSpec extends FunSuite { cd1 -> ucd1 ) - import fr.acinq.eclair.wire.QueryFlagTypes._ + import fr.acinq.eclair.wire.QueryShortChannelIdsTlv.QueryFlagType._ - assert(Router.getChannelDigestInfo(channels, updates)(ab.shortChannelId) == TimestampsAndChecksums(now, 714408668, now, 714408668)) + assert(Router.getChannelDigestInfo(channels, updates)(ab.shortChannelId) == (Timestamps(now, now), Checksums(3297511804L, 3297511804L))) // no extended info but we know the channel: we ask for the updates - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) // same checksums, newer timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now + 1, 714408668, now + 1, 714408668))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(3297511804L, 3297511804L))) === 0.toByte) // different checksums, newer timestamps: we ask for the updates - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now + 1, 154654604, now, 714408668))) === INCLUDE_CHANNEL_UPDATE_1) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now, 714408668, now + 1, 45664546))) === INCLUDE_CHANNEL_UPDATE_2) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now + 1, 154654604, now + 1, 45664546+6))) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3297511804L))) === INCLUDE_CHANNEL_UPDATE_1) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(3297511804L, 45664546))) === INCLUDE_CHANNEL_UPDATE_2) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546+6))) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) // different checksums, older timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now - 1, 154654604, now, 714408668))) === 0.toByte) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now, 714408668, now - 1, 45664546))) === 0.toByte) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(TimestampsAndChecksums(now - 1, 154654604, now - 1, 45664546))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3297511804L))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(3297511804L, 45664546))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546))) === 0.toByte) // missing channel update: we ask for it - assert(Router.computeFlag(channels, updates)(cd.shortChannelId, Some(TimestampsAndChecksums(now, 714408668, now, 714408668))) === INCLUDE_CHANNEL_UPDATE_2) + assert(Router.computeFlag(channels, updates)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L))) === INCLUDE_CHANNEL_UPDATE_2) // unknown channel: we ask everything - assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None) === QueryFlagTypes.INCLUDE_ALL) + assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None, None) === INCLUDE_ALL) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 22e19db62c..95f451ee45 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -67,7 +67,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { def counts = BasicSyncResult(ranges.size, queries.size, channels.size, updates.size) } - def sync(src: TestFSMRef[State, Data, Router], tgt: TestFSMRef[State, Data, Router], extendedQueryFlags_opt: Option[ExtendedQueryFlags]): SyncResult = { + def sync(src: TestFSMRef[State, Data, Router], tgt: TestFSMRef[State, Data, Router], extendedQueryFlags_opt: Option[QueryChannelRangeTlv]): SyncResult = { val sender = TestProbe() val pipe = TestProbe() pipe.ignoreMsg { @@ -161,7 +161,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) val charlieId = randomKey.publicKey val sender = TestProbe() - val extendedQueryFlags_opt = Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS) + val extendedQueryFlags_opt = Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) // tell alice to sync with bob assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) @@ -227,7 +227,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) // router should ask for our first block of ids - assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.shortChannelIds, None)) + assert(transport.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.shortChannelIds, TlvStream.empty)) // router should think that it is missing 100 channels, in one request val Some(sync) = router.stateData.sync.get(remoteNodeId) assert(sync.total == 1) @@ -241,7 +241,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { test("sync progress") { - def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42))), None) + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42))), TlvStream.empty) val nodeidA = randomKey.publicKey val nodeidB = randomKey.publicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala new file mode 100644 index 0000000000..6d74284fc1 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala @@ -0,0 +1,94 @@ +package fr.acinq.eclair.wire + +import fr.acinq.bitcoin.Block +import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.eclair.wire.LightningMessageCodecs._ +import ReplyChannelRangeTlv._ +import org.scalatest.FunSuite +import scodec.bits.ByteVector + +class ExtendedQueriesCodecsSpec extends FunSuite { + test("encode query_short_channel_ids (no optional data)") { + val query_short_channel_id = QueryShortChannelIds( + Block.RegtestGenesisBlock.blockId, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + TlvStream.empty) + + val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require + val decoded = queryShortChannelIdsCodec.decode(encoded).require.value + assert(decoded === query_short_channel_id) + } + + test("encode query_short_channel_ids (with optional data)") { + val query_short_channel_id = QueryShortChannelIds( + Block.RegtestGenesisBlock.blockId, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)))) + + val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require + val decoded = queryShortChannelIdsCodec.decode(encoded).require.value + assert(decoded === query_short_channel_id) + } + + test("encode query_short_channel_ids (with optional data including unknown data)") { + val query_short_channel_id = QueryShortChannelIds( + Block.RegtestGenesisBlock.blockId, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + TlvStream( + QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)) :: Nil, + GenericTlv(UInt64(43), ByteVector.fromValidHex("deadbeef")) :: Nil + ) + ) + + val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require + val decoded = queryShortChannelIdsCodec.decode(encoded).require.value + assert(decoded === query_short_channel_id) + } + + test("encode reply_channel_range (no optional data)") { + val replyChannelRange = ReplyChannelRange( + Block.RegtestGenesisBlock.blockId, + 1, 100, + 1.toByte, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + None, None) + + val encoded = replyChannelRangeCodec.encode(replyChannelRange).require + val decoded = replyChannelRangeCodec.decode(encoded).require.value + assert(decoded === replyChannelRange) + } + + test("encode reply_channel_range (with optional timestamps)") { + val replyChannelRange = ReplyChannelRange( + Block.RegtestGenesisBlock.blockId, + 1, 100, + 1.toByte, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3)))), + None) + + val encoded = replyChannelRangeCodec.encode(replyChannelRange).require + val decoded = replyChannelRangeCodec.decode(encoded).require.value + assert(decoded === replyChannelRange) + } + + test("encode reply_channel_range (with optional timestamps, checksums, and unknown data)") { + val replyChannelRange = ReplyChannelRange( + Block.RegtestGenesisBlock.blockId, + 1, 100, + 1.toByte, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + TlvStream( + List( + EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3))), + EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) + ), + GenericTlv(UInt64(7), ByteVector.fromValidHex("deadbeef")) :: Nil + ) + ) + + val encoded = replyChannelRangeCodec.encode(replyChannelRange).require + val decoded = replyChannelRangeCodec.decode(encoded).require.value + assert(decoded === replyChannelRange) + } +} 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 3649943675..a8d7a05790 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 @@ -21,14 +21,9 @@ import java.net.{Inet4Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair._ -import fr.acinq.eclair.api._ -import fr.acinq.eclair.channel.State -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ -import org.json4s.JsonAST.{JNothing, JString} -import org.json4s.{CustomSerializer, ShortTypeHints} -import org.json4s.jackson.Serialization +import ReplyChannelRangeTlv._ import org.scalatest.FunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -77,9 +72,18 @@ class LightningMessageCodecsSpec extends FunSuite { val channel_update = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, MilliSatoshi(4), MilliSatoshi(5), 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32, ShortChannelId(42), randomBytes64, randomBytes64) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None) - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(1, 1, 1, 1), TimestampsAndChecksums(2, 2, 2, 2), TimestampsAndChecksums(3, 3, 3, 3))))) + val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) + val unknownTlv = GenericTlv(UInt64(5), ByteVector.fromValidHex("deadbeef")) + val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, + 100000, + 1500, + 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( + EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, + unknownTlv :: Nil) + ) val ping = Ping(100, bin(10, 1)) val pong = Pong(bin(10, 1)) val channel_reestablish = ChannelReestablish(randomBytes32, 242842L, 42L) @@ -100,11 +104,16 @@ class LightningMessageCodecsSpec extends FunSuite { test("non-reg encoding type") { val refs = Map( - hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None), - hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None), - hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4000400010204" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), - hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3000c01789c6364620100000e0008" -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4010400010204" + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3010c01789c6364620100000e0008" + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) ) + refs.forall { case (bin, obj) => lightningMessageCodec.decode(bin.toBitVector).require.value == obj && lightningMessageCodec.encode(obj).require == bin.toBitVector @@ -113,18 +122,33 @@ class LightningMessageCodecsSpec extends FunSuite { case class TestItem(msg: Any, hex: String) - ignore("test vectors") { - - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, None) - val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 35000, 100, Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS)) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 756230, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), None) - val reply_channel_range_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 1600, 110, 1, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), None) - val reply_channel_range_timestamps_checksums = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 122334, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(164545, 1111, 948165, 2222), TimestampsAndChecksums(489645, 3333, 4786864, 4444), TimestampsAndChecksums(46456, 5555, 9788415, 6666))))) - val reply_channel_range_timestamps_checksums_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 500, 100, 1, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(1234545), ShortChannelId(4897484), ShortChannelId(4564676))), Some(ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS), Some(ExtendedInfo(List(TimestampsAndChecksums(164545, 1111, 948165, 2222), TimestampsAndChecksums(489645, 3333, 4786864, 4444), TimestampsAndChecksums(46456, 5555, 9788415, 6666))))) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None) - val query_short_channel_id_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), None) - val query_short_channel_id_flags = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), Some(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))), Some(EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) + test("test vectors") { + import org.json4s.{CustomSerializer, ShortTypeHints} + import org.json4s.JsonAST.JString + import org.json4s.jackson.Serialization + import fr.acinq.eclair.api._ + + val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, TlvStream.empty) + val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, + 35000, + 100, + 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, + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), None, None) + val reply_channel_range_timestamps_checksums = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 122334, 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545, 948165), Timestamps(489645, 4786864), Timestamps(46456, 9788415)))), + Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) + val reply_channel_range_timestamps_checksums_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 122334, 1500, 1, + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545, 948165), Timestamps(489645, 4786864), Timestamps(46456, 9788415)))), + Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) + val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) + val query_short_channel_id_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), TlvStream.empty) + 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 = List( query_channel_range, @@ -139,13 +163,18 @@ class LightningMessageCodecsSpec extends FunSuite { query_short_channel_id_flags_zlib ) - class EncodingTypeSerializer extends CustomSerializer[EncodingType](format => ({ null }, { + class EncodingTypeSerializer extends CustomSerializer[EncodingType](format => ( { + null + }, { case EncodingType.UNCOMPRESSED => JString("UNCOMPRESSED") case EncodingType.COMPRESSED_ZLIB => JString("COMPRESSED_ZLIB") })) - class ExtendedQueryFlagsSerializer extends CustomSerializer[ExtendedQueryFlags](format => ({ null }, { - case ExtendedQueryFlags.TIMESTAMPS_AND_CHECKSUMS => JString("TIMESTAMPS_AND_CHECKSUMS") + class ExtendedQueryFlagsSerializer extends CustomSerializer[QueryChannelRangeTlv.QueryFlags](format => ( { + null + }, { + case QueryChannelRangeTlv.QueryFlags(flag) => + JString(((if (QueryChannelRangeTlv.QueryFlags.wantTimestamps(flag)) List("WANT_TIMESTAMPS") else List()) ::: (if (QueryChannelRangeTlv.QueryFlags.wantChecksums(flag)) List("WANT_CHECKSUMS") else List())) mkString (" | ")) })) implicit val formats = org.json4s.DefaultFormats.withTypeHintFieldName("type") + @@ -174,16 +203,16 @@ class LightningMessageCodecsSpec extends FunSuite { new DirectionSerializer + new PaymentRequestSerializer + ShortTypeHints(List( - classOf[QueryChannelRange], - classOf[ReplyChannelRange], - classOf[QueryShortChannelIds])) - - refs.foreach { - obj => - val bin = lightningMessageCodec.encode(obj).require - println(Serialization.writePretty(TestItem(obj, bin.toHex))) - } + classOf[QueryChannelRange], + classOf[ReplyChannelRange], + classOf[QueryShortChannelIds])) + val items = refs.map { obj => + val bin = lightningMessageCodec.encode(obj).require + TestItem(obj, bin.toHex) + } + val json = Serialization.writePretty(items) + println(json) } test("decode channel_update with htlc_maximum_msat") { @@ -196,4 +225,5 @@ class LightningMessageCodecsSpec extends FunSuite { val bin2 = ByteVector(lightningMessageCodec.encode(update).require.toByteArray) assert(bin === bin2) } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala index e2c5dbce1b..c21033085e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala @@ -294,6 +294,12 @@ class TlvCodecsSpec extends FunSuite { } } + test("get optional TLV field") { + val stream = TlvStream[TestTlv](Seq(TestType254(42), TestType1(42)), Seq(GenericTlv(13, hex"2a"), GenericTlv(11, hex"2b"))) + assert(stream.get[TestType254] == Some(TestType254(42))) + assert(stream.get[TestType1] == Some(TestType1(42))) + assert(stream.get[TestType2] == None) + } } object TlvCodecsSpec { From 92d9f2a887a23f3c218ffb54eddb208bf44494d3 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Mon, 26 Aug 2019 15:18:27 +0200 Subject: [PATCH 66/86] Channel range queries: send back node announcements (#1108) * Channel Range Queries: send back node announcements if requested This PR adds support for sending back node announcements when replying to channel range queries: - when explicitly requested (bit is set in the optional query flag) - when query flags are not used and a channel announcement is sent (as per the BOLTs) A new configuration option `request-node-announcements` has been added in the `router` section. If set to true, we will request node announcements when we receive a channel id (through channel range queries) that we don't know of. This is a setting that we will probably turn off on mobile devices. * Increase tests timeouts There is now more work to do. * Test query sync with and without node announcements * Router: minor fix * Router: rework query handling --- eclair-core/src/main/resources/reference.conf | 1 + .../scala/fr/acinq/eclair/NodeParams.scala | 1 + .../scala/fr/acinq/eclair/router/Router.scala | 194 +++++++++++++----- .../eclair/wire/QueryShortChannelIdsTlv.scala | 11 +- .../scala/fr/acinq/eclair/TestConstants.scala | 2 + .../router/ChannelRangeQueriesSpec.scala | 75 +++++-- .../acinq/eclair/router/RoutingSyncSpec.scala | 100 ++++++--- 7 files changed, 284 insertions(+), 100 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index cadbbec31b..49898092e1 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -116,6 +116,7 @@ eclair { channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration broadcast-interval = 60 seconds // see BOLT #7 init-timeout = 5 minutes + request-node-announcements = true // if true we will ask for node announcements when we receive channel ids that we don't know // the values below will be used to perform route searching path-finding { 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 093d3b8a64..c66ee80d76 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -235,6 +235,7 @@ object NodeParams { channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"), + requestNodeAnnouncements = config.getBoolean("router.request-node-announcements"), searchMaxRouteLength = config.getInt("router.path-finding.max-route-length"), searchMaxCltv = config.getInt("router.path-finding.max-cltv"), searchMaxFeeBase = Satoshi(config.getLong("router.path-finding.fee-threshold-sat")), 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 3d80cba7e0..4c4ceda9ef 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 @@ -50,6 +50,7 @@ import scala.util.{Random, Try} case class RouterConf(randomizeRouteSelection: Boolean, channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, + requestNodeAnnouncements: Boolean, searchMaxFeeBase: Satoshi, searchMaxFeePct: Double, searchMaxRouteLength: Int, @@ -535,7 +536,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ ids match { case Nil => acc.reverse case head :: tail => - val flag = computeFlag(d.channels, d.updates)(head, timestamps.headOption, checksums.headOption) + val flag = computeFlag(d.channels, d.updates)(head, timestamps.headOption, checksums.headOption, nodeParams.routerConf.requestNodeAnnouncements) // 0 means nothing to query, just don't include it val acc1 = if (flag != 0) ShortChannelIdAndFlag(head, flag) :: acc else acc loop(tail, timestamps.drop(1), checksums.drop(1), acc1) @@ -549,7 +550,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) { case ((c, u), ShortChannelIdAndFlag(_, flag)) => - val c1 = c + (if (QueryShortChannelIdsTlv.QueryFlagType.includeAnnouncement(flag)) 1 else 0) + val c1 = c + (if (QueryShortChannelIdsTlv.QueryFlagType.includeChannelAnnouncement(flag)) 1 else 0) val u1 = u + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate1(flag)) 1 else 0) + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate2(flag)) 1 else 0) (c1, u1) } @@ -573,26 +574,29 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, shortChannelIds, queryFlags_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) - val (channelCount, updatesCount) = shortChannelIds.array - .zipWithIndex - .foldLeft((0, 0)) { - case ((c, u), (shortChannelId, idx)) => - var c1 = c - var u1 = u - val flag = routingMessage.queryFlags_opt.map(_.array(idx)).getOrElse(QueryShortChannelIdsTlv.QueryFlagType.INCLUDE_ALL) - d.channels.get(shortChannelId) match { - case None => log.warning("received query for shortChannelId={} that we don't have", shortChannelId) - case Some(ca) => - if (QueryShortChannelIdsTlv.QueryFlagType.includeAnnouncement(flag)) { - transport ! ca - c1 = c1 + 1 - } - if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate1(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => transport ! u; u1 = u1 + 1 } - if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate2(flag)) d.updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => transport ! u; u1 = u1 + 1 } - } - (c1, u1) + val flags = routingMessage.queryFlags_opt.map(_.array).getOrElse(List.empty[Long]) + + var channelCount = 0 + var updateCount = 0 + var nodeCount = 0 + + Router.handleQuery(d.nodes, d.channels, d.updates)( + shortChannelIds.array, + flags, + ca => { + channelCount = channelCount + 1 + transport ! ca + }, + cu => { + updateCount = updateCount + 1 + transport ! cu + }, + na => { + nodeCount = nodeCount + 1 + transport ! na } - log.info("received query_short_channel_ids with {} items, sent back {} channels and {} updates", shortChannelIds.array.size, channelCount, updatesCount) + ) + log.info("received query_short_channel_ids with {} items, sent back {} channels and {} updates and {} nodes", shortChannelIds.array.size, channelCount, updateCount, nodeCount) transport ! ReplyShortChannelIdsEnd(chainHash, 1) stay @@ -853,43 +857,135 @@ object Router { height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } - def computeFlag(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])( - shortChannelId: ShortChannelId, - timestamps_opt: Option[ReplyChannelRangeTlv.Timestamps], - checksums_opt: Option[ReplyChannelRangeTlv.Checksums]): Long = { - import QueryShortChannelIdsTlv.QueryFlagType - var flag = 0L - (timestamps_opt, checksums_opt) match { - case (Some(theirTimestamps), Some(theirChecksums)) if channels.contains(shortChannelId) => - val (ourTimestamps, ourChecksums) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + def shouldRequestUpdate(ourTimestamp: Long, ourChecksum: Long, theirTimestamp_opt: Option[Long], theirChecksum_opt: Option[Long]): Boolean = { + (theirTimestamp_opt, theirChecksum_opt) match { + case (Some(theirTimestamp), Some(theirChecksum)) => // we request their channel_update if all those conditions are met: // - it is more recent than ours // - it is different from ours, or it is the same but ours is about to be stale - // - it is not stale itself - if (ourTimestamps.timestamp1 < theirTimestamps.timestamp1 && (ourChecksums.checksum1 != theirChecksums.checksum1 || isAlmostStale(ourTimestamps.timestamp1)) && !isStale(theirTimestamps.timestamp1)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 - if (ourTimestamps.timestamp2 < theirTimestamps.timestamp2 && (ourChecksums.checksum2 != theirChecksums.checksum2 || isAlmostStale(ourTimestamps.timestamp1)) && !isStale(theirTimestamps.timestamp2)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 - case (Some(theirTimestamps), None) if channels.contains(shortChannelId) => - val (ourTimestamps, _) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) - // we request their channel_update if all those conditions are met: - // - it is more recent than ours - // - it is not stale itself - if (ourTimestamps.timestamp1 < theirTimestamps.timestamp1 && !isStale(theirTimestamps.timestamp1)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 - if (ourTimestamps.timestamp2 < theirTimestamps.timestamp2 && !isStale(theirTimestamps.timestamp2)) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 - case (None, Some(theirChecksums)) if channels.contains(shortChannelId) => - val (_, ourChecksums) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + // - it is not stale + val theirsIsMoreRecent = ourTimestamp < theirTimestamp + val theirsIsDifferent = ourChecksum != theirChecksum + val oursIsAlmostStale = isAlmostStale(ourTimestamp) + val theirsIsStale = isStale(theirTimestamp) + theirsIsMoreRecent && (theirsIsDifferent || oursIsAlmostStale) && !theirsIsStale + case (Some(theirTimestamp), None) => + val theirsIsMoreRecent = ourTimestamp < theirTimestamp + val theirsIsStale = isStale(theirTimestamp) + theirsIsMoreRecent && !theirsIsStale + case (None, Some(theirChecksum)) => // this should not happen as we will not ask for checksums without asking for timestamps too - if (ourChecksums.checksum1 != theirChecksums.checksum1 && theirChecksums.checksum1 != 0) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 - if (ourChecksums.checksum2 != theirChecksums.checksum2 && theirChecksums.checksum2 != 0) flag = flag | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 - case (None, None) if channels.contains(shortChannelId) => - // we know this channel: we only request their channel updates - flag = QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 + val theirsIsDifferent = theirChecksum != 0 && ourChecksum != theirChecksum + theirsIsDifferent case _ => - // we don't know this channel: we request everything - flag = QueryFlagType.INCLUDE_CHANNEL_ANNOUNCEMENT | QueryFlagType.INCLUDE_CHANNEL_UPDATE_1 | QueryFlagType.INCLUDE_CHANNEL_UPDATE_2 + // they did not include timestamp or checksum => ask for the update + true + } + } + + def computeFlag(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])( + shortChannelId: ShortChannelId, + theirTimestamps_opt: Option[ReplyChannelRangeTlv.Timestamps], + theirChecksums_opt: Option[ReplyChannelRangeTlv.Checksums], + includeNodeAnnouncements: Boolean): Long = { + import QueryShortChannelIdsTlv.QueryFlagType._ + + val flag = channels.contains(shortChannelId) match { + case false if includeNodeAnnouncements => + INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2 + case false => + INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 + case true => + // we already know this channel + val (ourTimestamps, ourChecksums) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + // if they don't provide timestamps or checksums, we set appropriate default values: + // - we assume their timestamp is more recent than ours by setting timestamp = Long.MaxValue + // - we assume their update is different from ours by setting checkum = Long.MaxValue (NB: our default value for checksum is 0) + val shouldRequestUpdate1 = shouldRequestUpdate(ourTimestamps.timestamp1, ourChecksums.checksum1, theirTimestamps_opt.map(_.timestamp1), theirChecksums_opt.map(_.checksum1)) + val shouldRequestUpdate2 = shouldRequestUpdate(ourTimestamps.timestamp2, ourChecksums.checksum2, theirTimestamps_opt.map(_.timestamp2), theirChecksums_opt.map(_.checksum2)) + val flagUpdate1 = if (shouldRequestUpdate1) INCLUDE_CHANNEL_UPDATE_1 else 0 + val flagUpdate2 = if (shouldRequestUpdate2) INCLUDE_CHANNEL_UPDATE_2 else 0 + flagUpdate1 | flagUpdate2 } flag } + /** + * Handle a query message, which includes a list of channel ids and flags. + * + * @param nodes node id -> node announcement + * @param channels channel id -> channel announcement + * @param updates channel description -> channel update + * @param ids list of channel ids + * @param flags list of query flags, either empty one flag per channel id + * @param onChannel called when a channel announcement matches (i.e. its bit is set in the query flag and we have it) + * @param onUpdate called when a channel update matches + * @param onNode called when a node announcement matches + * + */ + def handleQuery(nodes: Map[PublicKey, NodeAnnouncement], + channels: SortedMap[ShortChannelId, ChannelAnnouncement], + updates: Map[ChannelDesc, ChannelUpdate])( + ids: List[ShortChannelId], + flags: List[Long], + onChannel: ChannelAnnouncement => Unit, + onUpdate: ChannelUpdate => Unit, + onNode: NodeAnnouncement => Unit): Unit = { + import QueryShortChannelIdsTlv.QueryFlagType + + // we loop over channel ids and query flag. We track node Ids for node announcement + // we've already sent to avoid sending them multiple times, as requested by the BOLTs + @tailrec + def loop(ids: List[ShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { + case Nil => (numca, numcu, nodesSent.size) + case head :: tail if !channels.contains(head) => + //log.warning("received query for shortChannelId={} that we don't have", head) + loop(tail, flags.drop(1), numca, numcu, nodesSent) + case head :: tail => + var numca1 = numca + var numcu1 = numcu + var sent1 = nodesSent + val ca = channels(head) + val flag_opt = flags.headOption + // no flag means send everything + + val includeChannel = flag_opt.forall(QueryFlagType.includeChannelAnnouncement) + val includeUpdate1 = flag_opt.forall(QueryFlagType.includeUpdate1) + val includeUpdate2 = flag_opt.forall(QueryFlagType.includeUpdate2) + val includeNode1 = flag_opt.forall(QueryFlagType.includeNodeAnnouncement1) + val includeNode2 = flag_opt.forall(QueryFlagType.includeNodeAnnouncement2) + + if (includeChannel) { + onChannel(ca) + } + if (includeUpdate1) { + updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)).foreach { u => + onUpdate(u) + } + } + if (includeUpdate2) { + updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)).foreach { u => + onUpdate(u) + } + } + if (includeNode1 && !sent1.contains(ca.nodeId1)) { + nodes.get(ca.nodeId1).foreach { n => + onNode(n) + sent1 = sent1 + ca.nodeId1 + } + } + if (includeNode2 && !sent1.contains(ca.nodeId2)) { + nodes.get(ca.nodeId2).foreach { n => + onNode(n) + sent1 = sent1 + ca.nodeId2 + } + } + loop(tail, flags.drop(1), numca1, numcu1, sent1) + } + + loop(ids, flags) + } + /** * Returns overall progress on synchronization * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala index 3c878b2d7c..12f5ad96fa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.wire import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.wire.CommonCodecs.{shortchannelid, varint, varintoverflow} +import fr.acinq.eclair.wire.CommonCodecs.{varint, varintoverflow} import scodec.Codec import scodec.codecs.{byte, discriminated, list, provide, variableSizeBytesLong, zlib} @@ -20,13 +20,18 @@ object QueryShortChannelIdsTlv { val INCLUDE_CHANNEL_ANNOUNCEMENT: Long = 1 val INCLUDE_CHANNEL_UPDATE_1: Long = 2 val INCLUDE_CHANNEL_UPDATE_2: Long = 4 - val INCLUDE_ALL: Long = (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2) + val INCLUDE_NODE_ANNOUNCEMENT_1: Long = 8 + val INCLUDE_NODE_ANNOUNCEMENT_2: Long = 16 - def includeAnnouncement(flag: Long) = (flag & INCLUDE_CHANNEL_ANNOUNCEMENT) != 0 + def includeChannelAnnouncement(flag: Long) = (flag & INCLUDE_CHANNEL_ANNOUNCEMENT) != 0 def includeUpdate1(flag: Long) = (flag & INCLUDE_CHANNEL_UPDATE_1) != 0 def includeUpdate2(flag: Long) = (flag & INCLUDE_CHANNEL_UPDATE_2) != 0 + + def includeNodeAnnouncement1(flag: Long) = (flag & INCLUDE_NODE_ANNOUNCEMENT_1) != 0 + + def includeNodeAnnouncement2(flag: Long) = (flag & INCLUDE_NODE_ANNOUNCEMENT_2) != 0 } val encodedQueryFlagsCodec: Codec[EncodedQueryFlags] = 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 9c340e5a8e..f909aa70e0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -106,6 +106,7 @@ object TestConstants { randomizeRouteSelection = false, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, + requestNodeAnnouncements = true, searchMaxFeeBase = Satoshi(21), searchMaxFeePct = 0.03, searchMaxCltv = 2016, @@ -176,6 +177,7 @@ object TestConstants { randomizeRouteSelection = false, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, + requestNodeAnnouncements = true, searchMaxFeeBase = Satoshi(21), searchMaxFeePct = 0.03, searchMaxCltv = 2016, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 065ee73ac3..f7f009e970 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.router import fr.acinq.eclair.wire.ReplyChannelRangeTlv._ -import fr.acinq.eclair.wire._ import fr.acinq.eclair.{MilliSatoshi, randomKey} import org.scalatest.FunSuite @@ -27,21 +26,63 @@ import scala.compat.Platform class ChannelRangeQueriesSpec extends FunSuite { - test("compute flag tests") { + test("ask for update test") { + // they don't provide anything => we always ask for the update + assert(Router.shouldRequestUpdate(0, 0, None, None)) + assert(Router.shouldRequestUpdate(Int.MaxValue, 12345, None, None)) + + // their update is older => don't ask + val now = Platform.currentTime / 1000 + assert(!Router.shouldRequestUpdate(now, 0, Some(now - 1), None)) + assert(!Router.shouldRequestUpdate(now, 0, Some(now - 1), Some(12345))) + assert(!Router.shouldRequestUpdate(now, 12344, Some(now - 1), None)) + assert(!Router.shouldRequestUpdate(now, 12344, Some(now - 1), Some(12345))) + + // their update is newer but stale => don't ask + val old = now - 4 * 2016 * 24 * 3600 + assert(!Router.shouldRequestUpdate(old - 1, 0, Some(old), None)) + assert(!Router.shouldRequestUpdate(old - 1, 0, Some(old), Some(12345))) + assert(!Router.shouldRequestUpdate(old - 1, 12344, Some(old), None)) + assert(!Router.shouldRequestUpdate(old - 1, 12344, Some(old), Some(12345))) + + // their update is newer but with the same checksum, and ours is stale or about to be => ask (we want to renew our update) + assert(Router.shouldRequestUpdate(old, 12345, Some(now), Some(12345))) + + // their update is newer but with the same checksum => don't ask + assert(!Router.shouldRequestUpdate(now - 1, 12345, Some(now), Some(12345))) + + // their update is newer with a different checksum => always ask + assert(Router.shouldRequestUpdate(now - 1, 0, Some(now), None)) + assert(Router.shouldRequestUpdate(now - 1, 0, Some(now), Some(12345))) + assert(Router.shouldRequestUpdate(now - 1, 12344, Some(now), None)) + assert(Router.shouldRequestUpdate(now - 1, 12344, Some(now), Some(12345))) + + // they just provided a 0 checksum => don't ask + assert(!Router.shouldRequestUpdate(0, 0, None, Some(0))) + assert(!Router.shouldRequestUpdate(now, 1234, None, Some(0))) + + // they just provided a checksum that is the same as us => don't ask + assert(!Router.shouldRequestUpdate(now, 1234, None, Some(1234))) + + // they just provided a different checksum that is the same as us => ask + assert(Router.shouldRequestUpdate(now, 1234, None, Some(1235))) + } + + test("compute flag tests") { val now = Platform.currentTime / 1000 val a = randomKey.publicKey val b = randomKey.publicKey val ab = RouteCalculationSpec.makeChannel(123466L, a, b) - val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, MilliSatoshi(0), 0, timestamp = now) - val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, MilliSatoshi(0), 0, timestamp = now) + val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, MilliSatoshi(0), 0, timestamp = now) + val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, MilliSatoshi(0), 0, timestamp = now) val c = randomKey.publicKey val d = randomKey.publicKey val cd = RouteCalculationSpec.makeChannel(451312L, c, d) - val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, MilliSatoshi(0), 0, timestamp = now) - val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, MilliSatoshi(0), 0, timestamp = now) + val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, MilliSatoshi(0), 0, timestamp = now) + val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, MilliSatoshi(0), 0, timestamp = now) val e = randomKey.publicKey val f = randomKey.publicKey @@ -63,23 +104,23 @@ class ChannelRangeQueriesSpec extends FunSuite { assert(Router.getChannelDigestInfo(channels, updates)(ab.shortChannelId) == (Timestamps(now, now), Checksums(3297511804L, 3297511804L))) // no extended info but we know the channel: we ask for the updates - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) // same checksums, newer timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(3297511804L, 3297511804L))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(3297511804L, 3297511804L)), true) === 0.toByte) // different checksums, newer timestamps: we ask for the updates - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3297511804L))) === INCLUDE_CHANNEL_UPDATE_1) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(3297511804L, 45664546))) === INCLUDE_CHANNEL_UPDATE_2) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546+6))) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3297511804L)), true) === INCLUDE_CHANNEL_UPDATE_1) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(3297511804L, 45664546)), true) === INCLUDE_CHANNEL_UPDATE_2) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) // different checksums, older timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3297511804L))) === 0.toByte) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(3297511804L, 45664546))) === 0.toByte) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546))) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3297511804L)), true) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(3297511804L, 45664546)), true) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0.toByte) // missing channel update: we ask for it - assert(Router.computeFlag(channels, updates)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L))) === INCLUDE_CHANNEL_UPDATE_2) + assert(Router.computeFlag(channels, updates)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), true) === INCLUDE_CHANNEL_UPDATE_2) // unknown channel: we ask everything - assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None, None) === INCLUDE_ALL) - + assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 95f451ee45..2b2a0a0804 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -34,6 +34,7 @@ import org.scalatest.FunSuiteLike import scala.collection.immutable.TreeMap import scala.collection.{SortedSet, immutable, mutable} import scala.compat.Platform +import scala.concurrent.duration._ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { @@ -61,10 +62,10 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { } } - case class BasicSyncResult(ranges: Int, queries: Int, channels: Int, updates: Int) + case class BasicSyncResult(ranges: Int, queries: Int, channels: Int, updates: Int, nodes: Int) - case class SyncResult(ranges: Seq[ReplyChannelRange], queries: Seq[QueryShortChannelIds], channels: Seq[ChannelAnnouncement], updates: Seq[ChannelUpdate]) { - def counts = BasicSyncResult(ranges.size, queries.size, channels.size, updates.size) + case class SyncResult(ranges: Seq[ReplyChannelRange], queries: Seq[QueryShortChannelIds], channels: Seq[ChannelAnnouncement], updates: Seq[ChannelUpdate], nodes: Seq[NodeAnnouncement]) { + def counts = BasicSyncResult(ranges.size, queries.size, channels.size, updates.size, nodes.size) } def sync(src: TestFSMRef[State, Data, Router], tgt: TestFSMRef[State, Data, Router], extendedQueryFlags_opt: Option[QueryChannelRangeTlv]): SyncResult = { @@ -92,6 +93,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { var queries = Vector.empty[QueryShortChannelIds] var channels = Vector.empty[ChannelAnnouncement] var updates = Vector.empty[ChannelUpdate] + var nodes = Vector.empty[NodeAnnouncement] while (src.stateData.sync.nonEmpty) { // for each chunk, src sends a query_short_channel_id val query = pipe.expectMsgType[QueryShortChannelIds] @@ -104,6 +106,9 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { case u: ChannelUpdate => updates = updates :+ u u + case n: NodeAnnouncement => + nodes = nodes :+ n + n } // tgt replies with announcements announcements.foreach(ann => pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, ann))) @@ -111,10 +116,10 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val rscie = pipe.expectMsgType[ReplyShortChannelIdsEnd] pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, rscie)) } - SyncResult(rcrs, queries, channels, updates) + SyncResult(rcrs, queries, channels, updates, nodes) } - test("handle channel range extended") { + test("sync with standard channel queries") { val watcher = system.actorOf(Props(new YesWatcher())) val alice = TestFSMRef(new Router(Alice.nodeParams, watcher)) val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) @@ -123,76 +128,100 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val extendedQueryFlags_opt = None // tell alice to sync with bob - assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels) awaitCond(alice.stateData.updates === bob.stateData.updates) + awaitCond(alice.stateData.nodes === bob.stateData.nodes) // add some channels and updates to bob and resync - fakeRoutingInfo.take(40).map(_._2._1).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) - fakeRoutingInfo.take(40).map(_._2._2).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + fakeRoutingInfo.take(40).values.foreach { + case (ca, cu1, cu2, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, ca)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, cu1)) + // we don't send channel_update #2 + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) + } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 40) - assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40, nodes = 80) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels) awaitCond(alice.stateData.updates === bob.stateData.updates) // add some updates to bob and resync - fakeRoutingInfo.take(40).map(_._2._3).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + fakeRoutingInfo.take(40).values.foreach { + case (ca, cu1, cu2, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, cu2)) + } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 80) - assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 80) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 80, nodes = 80) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels) awaitCond(alice.stateData.updates === bob.stateData.updates) // add everything (duplicates will be ignored) fakeRoutingInfo.values.foreach { - case (c, u1, u2, _, _) => + case (c, u1, u2, na1, na2) => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u2)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size) - assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) + assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) } - test("handle channel range extended (extended)") { + def syncWithExtendedQueries(requestChannelAnnouncements: Boolean) = { val watcher = system.actorOf(Props(new YesWatcher())) - val alice = TestFSMRef(new Router(Alice.nodeParams, watcher)) + val alice = TestFSMRef(new Router(Alice.nodeParams.copy(routerConf = Alice.nodeParams.routerConf.copy(requestNodeAnnouncements = requestChannelAnnouncements)), watcher)) val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) val charlieId = randomKey.publicKey val sender = TestProbe() val extendedQueryFlags_opt = Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) // tell alice to sync with bob - assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels) awaitCond(alice.stateData.updates === bob.stateData.updates) // add some channels and updates to bob and resync - fakeRoutingInfo.take(40).map(_._2._1).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) - fakeRoutingInfo.take(40).map(_._2._2).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + fakeRoutingInfo.take(40).values.foreach { + case (ca, cu1, cu2, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, ca)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, cu1)) + // we don't send channel_update #2 + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) + } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 40) - assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40, nodes = if (requestChannelAnnouncements) 80 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) + if (requestChannelAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) // add some updates to bob and resync - fakeRoutingInfo.take(40).map(_._2._3).foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) + fakeRoutingInfo.take(40).values.foreach { + case (ca, cu1, cu2, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, cu2)) + } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 80) - assert(BasicSyncResult(ranges = 1, queries = 1, channels = 0, updates = 40) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 0, updates = 40, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) // add everything (duplicates will be ignored) fakeRoutingInfo.values.foreach { - case (c, u1, u2, _, _) => + case (c, u1, u2, na1, na2) => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, u2)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size) - assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40)) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) + assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40), nodes = if (requestChannelAnnouncements) 2 * (fakeRoutingInfo.size - 40) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) // bump random channel_updates @@ -203,9 +232,18 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val bumpedUpdates = (List(0, 42, 147, 153, 654, 834, 2301).map(touchUpdate(_, true)) ++ List(1, 42, 150, 200).map(touchUpdate(_, false))).toSet bumpedUpdates.foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) - assert(BasicSyncResult(ranges = 2, queries = 2, channels = 0, updates = bumpedUpdates.size) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + assert(BasicSyncResult(ranges = 2, queries = 2, channels = 0, updates = bumpedUpdates.size, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) + if (requestChannelAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) + } + + test("sync with extended channel queries (don't request node announcements)") { + syncWithExtendedQueries(false) + } + + test("sync with extended channel queries (request node announcements)") { + syncWithExtendedQueries(true) } test("reset sync state on reconnection") { From c8b41c9efaeac1fbc8a2b8fde709177fdc4b46c4 Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 26 Aug 2019 16:04:38 +0200 Subject: [PATCH 67/86] better naming --- .../scala/fr/acinq/eclair/router/Router.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 4c4ceda9ef..104d04efc6 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 @@ -865,20 +865,20 @@ object Router { // - it is different from ours, or it is the same but ours is about to be stale // - it is not stale val theirsIsMoreRecent = ourTimestamp < theirTimestamp - val theirsIsDifferent = ourChecksum != theirChecksum + val areDifferent = ourChecksum != theirChecksum val oursIsAlmostStale = isAlmostStale(ourTimestamp) val theirsIsStale = isStale(theirTimestamp) - theirsIsMoreRecent && (theirsIsDifferent || oursIsAlmostStale) && !theirsIsStale + theirsIsMoreRecent && (areDifferent || oursIsAlmostStale) && !theirsIsStale case (Some(theirTimestamp), None) => + // if we only have their timestamp, we request their channel_update if theirs is more recent than ours val theirsIsMoreRecent = ourTimestamp < theirTimestamp - val theirsIsStale = isStale(theirTimestamp) - theirsIsMoreRecent && !theirsIsStale + theirsIsMoreRecent case (None, Some(theirChecksum)) => - // this should not happen as we will not ask for checksums without asking for timestamps too - val theirsIsDifferent = theirChecksum != 0 && ourChecksum != theirChecksum - theirsIsDifferent - case _ => - // they did not include timestamp or checksum => ask for the update + // if we only have their checksum, we request their channel_update if it is different from ours + val areDifferent = ourChecksum != theirChecksum + areDifferent + case (None, None) => + // if we have neither their timestamp nor their checksum we request their channel_update true } } From d404e5d1b7f5a88d7e8ebf2a4693d756fc000d04 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 26 Aug 2019 16:43:52 +0200 Subject: [PATCH 68/86] Router: fix tests and rework computeFlags --- .../scala/fr/acinq/eclair/router/Router.scala | 39 ++++++++++--------- .../router/ChannelRangeQueriesSpec.scala | 19 ++++----- .../acinq/eclair/router/RoutingSyncSpec.scala | 16 ++++---- 3 files changed, 38 insertions(+), 36 deletions(-) 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 104d04efc6..1c6c425875 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 @@ -872,10 +872,11 @@ object Router { case (Some(theirTimestamp), None) => // if we only have their timestamp, we request their channel_update if theirs is more recent than ours val theirsIsMoreRecent = ourTimestamp < theirTimestamp - theirsIsMoreRecent + val theirsIsStale = isStale(theirTimestamp) + theirsIsMoreRecent && !theirsIsStale case (None, Some(theirChecksum)) => // if we only have their checksum, we request their channel_update if it is different from ours - val areDifferent = ourChecksum != theirChecksum + val areDifferent = theirChecksum != 0 && ourChecksum != theirChecksum areDifferent case (None, None) => // if we have neither their timestamp nor their checksum we request their channel_update @@ -890,24 +891,24 @@ object Router { includeNodeAnnouncements: Boolean): Long = { import QueryShortChannelIdsTlv.QueryFlagType._ - val flag = channels.contains(shortChannelId) match { - case false if includeNodeAnnouncements => - INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2 - case false => - INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 - case true => - // we already know this channel - val (ourTimestamps, ourChecksums) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) - // if they don't provide timestamps or checksums, we set appropriate default values: - // - we assume their timestamp is more recent than ours by setting timestamp = Long.MaxValue - // - we assume their update is different from ours by setting checkum = Long.MaxValue (NB: our default value for checksum is 0) - val shouldRequestUpdate1 = shouldRequestUpdate(ourTimestamps.timestamp1, ourChecksums.checksum1, theirTimestamps_opt.map(_.timestamp1), theirChecksums_opt.map(_.checksum1)) - val shouldRequestUpdate2 = shouldRequestUpdate(ourTimestamps.timestamp2, ourChecksums.checksum2, theirTimestamps_opt.map(_.timestamp2), theirChecksums_opt.map(_.checksum2)) - val flagUpdate1 = if (shouldRequestUpdate1) INCLUDE_CHANNEL_UPDATE_1 else 0 - val flagUpdate2 = if (shouldRequestUpdate2) INCLUDE_CHANNEL_UPDATE_2 else 0 - flagUpdate1 | flagUpdate2 + val flagsNodes = if (includeNodeAnnouncements) INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2 else 0 + + val flags = if (!channels.contains(shortChannelId)) { + INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 + } else { + // we already know this channel + val (ourTimestamps, ourChecksums) = Router.getChannelDigestInfo(channels, updates)(shortChannelId) + // if they don't provide timestamps or checksums, we set appropriate default values: + // - we assume their timestamp is more recent than ours by setting timestamp = Long.MaxValue + // - we assume their update is different from ours by setting checkum = Long.MaxValue (NB: our default value for checksum is 0) + val shouldRequestUpdate1 = shouldRequestUpdate(ourTimestamps.timestamp1, ourChecksums.checksum1, theirTimestamps_opt.map(_.timestamp1), theirChecksums_opt.map(_.checksum1)) + val shouldRequestUpdate2 = shouldRequestUpdate(ourTimestamps.timestamp2, ourChecksums.checksum2, theirTimestamps_opt.map(_.timestamp2), theirChecksums_opt.map(_.checksum2)) + val flagUpdate1 = if (shouldRequestUpdate1) INCLUDE_CHANNEL_UPDATE_1 else 0 + val flagUpdate2 = if (shouldRequestUpdate2) INCLUDE_CHANNEL_UPDATE_2 else 0 + flagUpdate1 | flagUpdate2 } - flag + + if (flags == 0) 0 else flags | flagsNodes } /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index f7f009e970..102f04115d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -104,20 +104,21 @@ class ChannelRangeQueriesSpec extends FunSuite { assert(Router.getChannelDigestInfo(channels, updates)(ab.shortChannelId) == (Timestamps(now, now), Checksums(3297511804L, 3297511804L))) // no extended info but we know the channel: we ask for the updates - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // same checksums, newer timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(3297511804L, 3297511804L)), true) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(3297511804L, 3297511804L)), true) === 0) // different checksums, newer timestamps: we ask for the updates - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3297511804L)), true) === INCLUDE_CHANNEL_UPDATE_1) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(3297511804L, 45664546)), true) === INCLUDE_CHANNEL_UPDATE_2) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2).toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3297511804L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(3297511804L, 45664546)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2| INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // different checksums, older timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3297511804L)), true) === 0.toByte) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(3297511804L, 45664546)), true) === 0.toByte) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0.toByte) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3297511804L)), true) === 0) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(3297511804L, 45664546)), true) === 0) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0) // missing channel update: we ask for it - assert(Router.computeFlag(channels, updates)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), true) === INCLUDE_CHANNEL_UPDATE_2) + assert(Router.computeFlag(channels, updates)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // unknown channel: we ask everything assert(Router.computeFlag(channels, updates)(ef.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 2b2a0a0804..32c84e6f6e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -172,9 +172,9 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { awaitCond(alice.stateData.updates === bob.stateData.updates) } - def syncWithExtendedQueries(requestChannelAnnouncements: Boolean) = { + def syncWithExtendedQueries(requestNodeAnnouncements: Boolean) = { val watcher = system.actorOf(Props(new YesWatcher())) - val alice = TestFSMRef(new Router(Alice.nodeParams.copy(routerConf = Alice.nodeParams.routerConf.copy(requestNodeAnnouncements = requestChannelAnnouncements)), watcher)) + val alice = TestFSMRef(new Router(Alice.nodeParams.copy(routerConf = Alice.nodeParams.routerConf.copy(requestNodeAnnouncements = requestNodeAnnouncements)), watcher)) val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) val charlieId = randomKey.publicKey val sender = TestProbe() @@ -195,10 +195,10 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 40) - assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40, nodes = if (requestChannelAnnouncements) 80 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40, nodes = if (requestNodeAnnouncements) 80 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) - if (requestChannelAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) + if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) // add some updates to bob and resync fakeRoutingInfo.take(40).values.foreach { @@ -206,7 +206,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, cu2)) } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 80) - assert(BasicSyncResult(ranges = 1, queries = 1, channels = 0, updates = 40, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 1, queries = 1, channels = 0, updates = 40, nodes = if (requestNodeAnnouncements) 80 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) @@ -220,7 +220,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) - assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40), nodes = if (requestChannelAnnouncements) 2 * (fakeRoutingInfo.size - 40) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 40) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) @@ -232,10 +232,10 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val bumpedUpdates = (List(0, 42, 147, 153, 654, 834, 2301).map(touchUpdate(_, true)) ++ List(1, 42, 150, 200).map(touchUpdate(_, false))).toSet bumpedUpdates.foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) - assert(BasicSyncResult(ranges = 2, queries = 2, channels = 0, updates = bumpedUpdates.size, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 2, queries = 2, channels = 0, updates = bumpedUpdates.size, nodes = if (requestNodeAnnouncements) 20 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) - if (requestChannelAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) + if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) } test("sync with extended channel queries (don't request node announcements)") { From 99478878a426718bb1357a25b306de7b5f0b0661 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 26 Aug 2019 16:46:55 +0200 Subject: [PATCH 69/86] Router: better naming --- .../scala/fr/acinq/eclair/router/Router.scala | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 1c6c425875..56f501b6e4 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 @@ -21,6 +21,7 @@ import java.util.zip.CRC32 import akka.Done import akka.actor.{ActorRef, Props, Status} import akka.event.Logging.MDC +import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} @@ -119,6 +120,9 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ import ExecutionContext.Implicits.global + // we pass these to helpers classes so that they have the logging context + implicit def implicitLog: LoggingAdapter = log + context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) context.system.eventStream.subscribe(self, classOf[LocalChannelDown]) @@ -580,7 +584,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ var updateCount = 0 var nodeCount = 0 - Router.handleQuery(d.nodes, d.channels, d.updates)( + Router.processChannelQuery(d.nodes, d.channels, d.updates)( shortChannelIds.array, flags, ca => { @@ -924,14 +928,14 @@ object Router { * @param onNode called when a node announcement matches * */ - def handleQuery(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, ChannelAnnouncement], - updates: Map[ChannelDesc, ChannelUpdate])( + def processChannelQuery(nodes: Map[PublicKey, NodeAnnouncement], + channels: SortedMap[ShortChannelId, ChannelAnnouncement], + updates: Map[ChannelDesc, ChannelUpdate])( ids: List[ShortChannelId], flags: List[Long], onChannel: ChannelAnnouncement => Unit, onUpdate: ChannelUpdate => Unit, - onNode: NodeAnnouncement => Unit): Unit = { + onNode: NodeAnnouncement => Unit)(implicit log: LoggingAdapter): Unit = { import QueryShortChannelIdsTlv.QueryFlagType // we loop over channel ids and query flag. We track node Ids for node announcement @@ -940,7 +944,7 @@ object Router { def loop(ids: List[ShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { case Nil => (numca, numcu, nodesSent.size) case head :: tail if !channels.contains(head) => - //log.warning("received query for shortChannelId={} that we don't have", head) + log.warning("received query for shortChannelId={} that we don't have", head) loop(tail, flags.drop(1), numca, numcu, nodesSent) case head :: tail => var numca1 = numca From ea110362c49aa5f0ed5cdd1a524204968e46becb Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 26 Aug 2019 19:17:38 +0200 Subject: [PATCH 70/86] Update comment [skip ci] --- eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala | 2 -- 1 file changed, 2 deletions(-) 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 6f76f5dfa6..d811f63c56 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 @@ -153,8 +153,6 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A } if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter - // NB: we always add extended info; if peer doesn't understand them it will ignore them - // README: for now we do not activate extended queries on mainnet val flags_opt = nodeParams.chainHash match { case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => From 217a8f75a9f362e3aab19dd1d61ae2653e4ff5e6 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 27 Aug 2019 15:31:59 +0200 Subject: [PATCH 71/86] Router: format code --- .../scala/fr/acinq/eclair/router/Router.scala | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) 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 de53cd6e53..f7d92b379e 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 @@ -568,7 +568,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(shortChannelIds.encoding, chunk.map(_.flag))) else TlvStream.empty - )) + )) .toList val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) // we only send a rely right away if there were no pending requests @@ -916,26 +916,26 @@ object Router { } /** - * Handle a query message, which includes a list of channel ids and flags. - * - * @param nodes node id -> node announcement - * @param channels channel id -> channel announcement - * @param updates channel description -> channel update - * @param ids list of channel ids - * @param flags list of query flags, either empty one flag per channel id - * @param onChannel called when a channel announcement matches (i.e. its bit is set in the query flag and we have it) - * @param onUpdate called when a channel update matches - * @param onNode called when a node announcement matches - * - */ + * Handle a query message, which includes a list of channel ids and flags. + * + * @param nodes node id -> node announcement + * @param channels channel id -> channel announcement + * @param updates channel description -> channel update + * @param ids list of channel ids + * @param flags list of query flags, either empty one flag per channel id + * @param onChannel called when a channel announcement matches (i.e. its bit is set in the query flag and we have it) + * @param onUpdate called when a channel update matches + * @param onNode called when a node announcement matches + * + */ def processChannelQuery(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])( - ids: List[ShortChannelId], - flags: List[Long], - onChannel: ChannelAnnouncement => Unit, - onUpdate: ChannelUpdate => Unit, - onNode: NodeAnnouncement => Unit)(implicit log: LoggingAdapter): Unit = { + ids: List[ShortChannelId], + flags: List[Long], + onChannel: ChannelAnnouncement => Unit, + onUpdate: ChannelUpdate => Unit, + onNode: NodeAnnouncement => Unit)(implicit log: LoggingAdapter): Unit = { import QueryShortChannelIdsTlv.QueryFlagType // we loop over channel ids and query flag. We track node Ids for node announcement @@ -992,11 +992,11 @@ object Router { } /** - * Returns overall progress on synchronization - * - * @param sync - * @return a sync progress indicator (1 means fully synced) - */ + * Returns overall progress on synchronization + * + * @param sync + * @return a sync progress indicator (1 means fully synced) + */ def syncProgress(sync: Map[PublicKey, Sync]): SyncProgress = { //NB: progress is in terms of requests, not individual channels val (pending, total) = sync.foldLeft((0, 0)) { @@ -1023,12 +1023,12 @@ object Router { } /** - * - * @param channels id -> announcement map - * @param updates channel updates - * @param id short channel id - * @return the timestamp of the most recent update for this channel id, 0 if we don't have any - */ + * + * @param channels id -> announcement map + * @param updates channel updates + * @param id short channel id + * @return the timestamp of the most recent update for this channel id, 0 if we don't have any + */ def getTimestamp(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(id: ShortChannelId): Long = { val ca = channels(id) val opt1 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)) @@ -1064,12 +1064,12 @@ object Router { case class ShortChannelIdsChunk(firstBlock: Long, numBlocks: Long, shortChannelIds: List[ShortChannelId]) /** - * Have to split ids because otherwise message could be too big - * there could be several reply_channel_range messages for a single query - * - * @param shortChannelIds - * @return - */ + * Have to split ids because otherwise message could be too big + * there could be several reply_channel_range messages for a single query + * + * @param shortChannelIds + * @return + */ def split(shortChannelIds: SortedSet[ShortChannelId]): List[ShortChannelIdsChunk] = { // TODO: this is wrong because it can split blocks if (shortChannelIds.isEmpty) { From 5d070d330d2472dbbb795a6fc5d2191c713dc08a Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 27 Aug 2019 16:46:51 +0200 Subject: [PATCH 72/86] Include chain hash in ChannelUpdate checksum --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 6 +++--- .../scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) 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 f7d92b379e..280cf2d2ea 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.router -import java.util.zip.CRC32 +import java.util.zip.CRC32C import akka.Done import akka.actor.{ActorRef, Props, Status} @@ -1055,8 +1055,8 @@ object Router { def getChecksum(u: ChannelUpdate): Long = { import u._ - val data = serializationResult(LightningMessageCodecs.channelUpdateChecksumCodec.encode(shortChannelId :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: HNil)) - val checksum = new CRC32() + val data = serializationResult(LightningMessageCodecs.channelUpdateChecksumCodec.encode(chainHash :: shortChannelId :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: HNil)) + val checksum = new CRC32C() checksum.update(data.toArray) checksum.getValue } 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 35f0eb4a50..5c65700c51 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 @@ -183,6 +183,7 @@ object LightningMessageCodecs { nodeAnnouncementWitnessCodec).as[NodeAnnouncement] val channelUpdateChecksumCodec = + ("chainHash" | bytes32) :: ("shortChannelId" | shortchannelid) :: (("messageFlags" | byte) >>:~ { messageFlags => ("channelFlags" | byte) :: From d3f283d7481fab4ac307e5386d1ac1a34ddd9c1b Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 27 Aug 2019 16:47:22 +0200 Subject: [PATCH 73/86] Extended Channel Queries: add CL interop test --- .../wire/ExtendedQueriesCodecsSpec.scala | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala index 6d74284fc1..672439717b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala @@ -1,9 +1,10 @@ package fr.acinq.eclair.wire -import fr.acinq.bitcoin.Block -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.LightningMessageCodecs._ -import ReplyChannelRangeTlv._ +import fr.acinq.eclair.wire.ReplyChannelRangeTlv._ +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} import org.scalatest.FunSuite import scodec.bits.ByteVector @@ -91,4 +92,46 @@ class ExtendedQueriesCodecsSpec extends FunSuite { val decoded = replyChannelRangeCodec.decode(encoded).require.value assert(decoded === replyChannelRange) } + + test("compute checksums correctly (CL test #1)") { + val update = ChannelUpdate( + chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), + signature = ByteVector64.fromValidHex("76df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c80293282"), + shortChannelId = ShortChannelId("103x1x0"), + timestamp = 1565587763L, + messageFlags = 0, + channelFlags = 0, + cltvExpiryDelta = CltvExpiryDelta(144), + htlcMinimumMsat = MilliSatoshi(0), + htlcMaximumMsat = None, + feeBaseMsat = MilliSatoshi(1000), + feeProportionalMillionths = 10 + ) + val check = ByteVector.fromValidHex("010276df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c8029328206226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00006700000100005d50f933000000900000000000000000000003e80000000a") + assert(LightningMessageCodecs.channelUpdateCodec.encode(update).require.bytes == check.drop(2)) + + val checksum = Router.getChecksum(update) + assert(checksum == 0x1112fa30L) + } + + test("compute checksums correctly (CL test #2)") { + val update = ChannelUpdate( + chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), + signature = ByteVector64.fromValidHex("06737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c"), + shortChannelId = ShortChannelId("109x1x0"), + timestamp = 1565587765L, + messageFlags = 1, + channelFlags = 0, + cltvExpiryDelta = CltvExpiryDelta(48), + htlcMinimumMsat = MilliSatoshi(0), + htlcMaximumMsat = Some(MilliSatoshi(100000)), + feeBaseMsat = MilliSatoshi(100), + feeProportionalMillionths = 11 + ) + val check = ByteVector.fromValidHex("010206737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00006d00000100005d50f935010000300000000000000000000000640000000b00000000000186a0") + assert(LightningMessageCodecs.channelUpdateCodec.encode(update).require.bytes == check.drop(2)) + + val checksum = Router.getChecksum(update) + assert(checksum == 0xf32ce968L) + } } From b937ac73b10851cc18f64c15ed60c51174fb8e9b Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 27 Aug 2019 17:16:27 +0200 Subject: [PATCH 74/86] Channel Queries: update test checksums --- .../eclair/router/ChannelRangeQueriesSpec.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 102f04115d..7428007a8f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -101,20 +101,20 @@ class ChannelRangeQueriesSpec extends FunSuite { import fr.acinq.eclair.wire.QueryShortChannelIdsTlv.QueryFlagType._ - assert(Router.getChannelDigestInfo(channels, updates)(ab.shortChannelId) == (Timestamps(now, now), Checksums(3297511804L, 3297511804L))) + assert(Router.getChannelDigestInfo(channels, updates)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 1697591108L))) // no extended info but we know the channel: we ask for the updates assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) assert(Router.computeFlag(channels, updates)(ab.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // same checksums, newer timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(3297511804L, 3297511804L)), true) === 0) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 1697591108L)), true) === 0) // different checksums, newer timestamps: we ask for the updates - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3297511804L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(3297511804L, 45664546)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 1697591108L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2| INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // different checksums, older timestamps: we don't ask anything - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3297511804L)), true) === 0) - assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(3297511804L, 45664546)), true) === 0) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 1697591108L)), true) === 0) + assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), true) === 0) assert(Router.computeFlag(channels, updates)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0) // missing channel update: we ask for it From 57abd93340ada4a213144976a05a7b386e2d24b5 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 10:20:10 +0200 Subject: [PATCH 75/86] minor log update --- eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 d811f63c56..695afc06b9 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 @@ -144,7 +144,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A if (remoteHasInitialRoutingSync) { if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries we do nothing, they will send us their filters - log.info("{} has set initial routing sync and supports channel range queries, we do nothing (they will send us a query)", remoteNodeId) + log.info("peer has set initial routing sync and supports channel range queries, we do nothing (they will send us a query)") } else { // "old" nodes, do as before log.info("peer requested a full routing table dump") @@ -153,13 +153,13 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A } if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter - // README: for now we do not activate extended queries on mainnet + // TODO: for now we do not activate extended queries on mainnet val flags_opt = nodeParams.chainHash match { case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => - log.info("using extended range queries") Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) case _ => None } + log.info(s"sending sync channel range query with flags_opt=$flags_opt") router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = flags_opt) } From b2b1506fd906ec2c243a87d307e75e548aa797fd Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 10:41:03 +0200 Subject: [PATCH 76/86] made encoding type configurable --- eclair-core/src/main/resources/reference.conf | 6 +++++- .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 10 ++++++++-- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 7 ++++--- .../src/test/scala/fr/acinq/eclair/TestConstants.scala | 4 +++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 49898092e1..bef04f1e63 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -116,7 +116,11 @@ eclair { channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration broadcast-interval = 60 seconds // see BOLT #7 init-timeout = 5 minutes - request-node-announcements = true // if true we will ask for node announcements when we receive channel ids that we don't know + + sync { + request-node-announcements = true // if true we will ask for node announcements when we receive channel ids that we don't know + encoding-type = zlib // encoding for short_channel_ids and timestamps in query channel sync messages; other possible value is "uncompressed" + } // the values below will be used to perform route searching path-finding { 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 b344358c5b..5b13c67c64 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.tor.Socks5ProxyParams -import fr.acinq.eclair.wire.{Color, NodeAddress} +import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress} import scodec.bits.ByteVector import scala.collection.JavaConversions._ @@ -191,6 +191,11 @@ object NodeParams { // to be below 0x100000000 msat which is about 42 mbtc require(feeBase <= MilliSatoshi(0xFFFFFFFFL), "fee-base-msat must be below 42 mbtc") + val routerSyncEncodingType = config.getString("router.sync.encoding-type") match { + case "uncompressed" => EncodingType.UNCOMPRESSED + case "zlib" => EncodingType.COMPRESSED_ZLIB + } + NodeParams( keyManager = keyManager, alias = nodeAlias, @@ -235,7 +240,8 @@ object NodeParams { channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"), - requestNodeAnnouncements = config.getBoolean("router.request-node-announcements"), + requestNodeAnnouncements = config.getBoolean("router.sync.request-node-announcements"), + encodingType = routerSyncEncodingType, searchMaxRouteLength = config.getInt("router.path-finding.max-route-length"), searchMaxCltv = CltvExpiryDelta(config.getInt("router.path-finding.max-cltv")), searchMaxFeeBase = Satoshi(config.getLong("router.path-finding.fee-threshold-sat")), 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 280cf2d2ea..89dc51037f 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 @@ -52,6 +52,7 @@ case class RouterConf(randomizeRouteSelection: Boolean, channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, requestNodeAnnouncements: Boolean, + encodingType: EncodingType, searchMaxFeeBase: Satoshi, searchMaxFeePct: Double, searchMaxRouteLength: Int, @@ -518,14 +519,14 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ case Some(extension) if extension.wantChecksums | extension.wantTimestamps => // we always compute timestamps and checksums even if we don't need both, overhead is negligible val (timestamps, checksums) = chunk.shortChannelIds.map(getChannelDigestInfo(d.channels, d.updates)).unzip - val encodedTimestamps = if (extension.wantTimestamps) Some(ReplyChannelRangeTlv.EncodedTimestamps(EncodingType.UNCOMPRESSED, timestamps)) else None + val encodedTimestamps = if (extension.wantTimestamps) Some(ReplyChannelRangeTlv.EncodedTimestamps(nodeParams.routerConf.encodingType, timestamps)) else None val encodedChecksums = if (extension.wantChecksums) Some(ReplyChannelRangeTlv.EncodedChecksums(checksums)) else None (encodedTimestamps, encodedChecksums) case _ => (None, None) } val reply = ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks, complete = 1, - shortChannelIds = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, chunk.shortChannelIds), + shortChannelIds = EncodedShortChannelIds(nodeParams.routerConf.encodingType, chunk.shortChannelIds), timestamps = timestamps, checksums = checksums) transport ! reply @@ -754,7 +755,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ // when we're sending updates to ourselves (transport_opt, remoteNodeId_opt) match { case (Some(transport), Some(remoteNodeId)) => - val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(u.shortChannelId)), TlvStream.empty) + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(nodeParams.routerConf.encodingType, List(u.shortChannelId)), TlvStream.empty) d.sync.get(remoteNodeId) match { case Some(sync) => // we already have a pending request to that node, let's add this channel to the list and we'll get it later 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 e2891ace34..cc38cb8e6f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf -import fr.acinq.eclair.wire.{Color, NodeAddress} +import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress} import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -107,6 +107,7 @@ object TestConstants { channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, requestNodeAnnouncements = true, + encodingType = EncodingType.COMPRESSED_ZLIB, searchMaxFeeBase = Satoshi(21), searchMaxFeePct = 0.03, searchMaxCltv = CltvExpiryDelta(2016), @@ -178,6 +179,7 @@ object TestConstants { channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, requestNodeAnnouncements = true, + encodingType = EncodingType.UNCOMPRESSED, searchMaxFeeBase = Satoshi(21), searchMaxFeePct = 0.03, searchMaxCltv = CltvExpiryDelta(2016), From bd86b06580ea3c61a3397cbb792ea62725f73743 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 11:16:34 +0200 Subject: [PATCH 77/86] renamed updateSync -> addToSync --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 4 ++-- .../test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 89dc51037f..922929b2df 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 @@ -571,7 +571,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ TlvStream.empty )) .toList - val (sync1, replynow_opt) = updateSync(d.sync, remoteNodeId, replies) + val (sync1, replynow_opt) = addToSync(d.sync, remoteNodeId, replies) // we only send a rely right away if there were no pending requests replynow_opt.foreach(transport ! _) context.system.eventStream.publish(syncProgress(sync1)) @@ -1088,7 +1088,7 @@ object Router { } } - def updateSync(syncMap: Map[PublicKey, Sync], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Sync], Option[RoutingMessage]) = { + def addToSync(syncMap: Map[PublicKey, Sync], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Sync], Option[RoutingMessage]) = { pending match { case head +: rest => // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 2c98079071..307b0b41f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -284,10 +284,10 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val nodeidA = randomKey.publicKey val nodeidB = randomKey.publicKey - val (sync1, _) = Router.updateSync(Map.empty, nodeidA, List(req, req, req, req)) + val (sync1, _) = Router.addToSync(Map.empty, nodeidA, List(req, req, req, req)) assert(Router.syncProgress(sync1) == SyncProgress(0.25D)) - val (sync2, _) = Router.updateSync(sync1, nodeidB, List(req, req, req, req, req, req, req, req, req, req, req, req)) + val (sync2, _) = Router.addToSync(sync1, nodeidB, List(req, req, req, req, req, req, req, req, req, req, req, req)) assert(Router.syncProgress(sync2) == SyncProgress(0.125D)) // let's assume we made some progress From 361460d3c5b3e9b25134e9b5fafe22b7594aba49 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 11:16:57 +0200 Subject: [PATCH 78/86] added rationale for split size --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 922929b2df..dd4eee4e01 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 @@ -1072,12 +1072,14 @@ object Router { * @return */ def split(shortChannelIds: SortedSet[ShortChannelId]): List[ShortChannelIdsChunk] = { - // TODO: this is wrong because it can split blocks + // this algorithm can split blocks (meaning that we can in theory generate several replies with the same first_block/num_blocks + // and a different set of short_channel_ids) but it doesn't matter + val SPLIT_SIZE = 3500 // we can theoretically fit 4091 uncompressed channel ids in a single lightning message (max size 65 Kb) if (shortChannelIds.isEmpty) { List(ShortChannelIdsChunk(0, 0, List.empty)) } else { shortChannelIds - .grouped(2000) // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid + .grouped(SPLIT_SIZE) .toList .map { group => // NB: group is never empty From 69f204850b9c96ce408add1cb5542899fb2f552c Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 12:06:43 +0200 Subject: [PATCH 79/86] fixed tests --- .../scala/fr/acinq/eclair/router/RoutingSyncSpec.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 307b0b41f8..13a8631f17 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -43,7 +43,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val fakeRoutingInfo: TreeMap[ShortChannelId, (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec .shortChannelIds - .take(2345) + .take(4567) .foldLeft(TreeMap.empty[ShortChannelId, (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement)]) { case (m, shortChannelId) => m + (shortChannelId -> makeFakeRoutingInfo(shortChannelId)) } @@ -167,7 +167,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) - assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 2, queries = 46, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) } @@ -220,7 +220,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) - assert(BasicSyncResult(ranges = 2, queries = 24, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 40) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) + assert(BasicSyncResult(ranges = 2, queries = 46, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 40) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) @@ -230,7 +230,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { makeNewerChannelUpdate(c, if (side) u1 else u2) } - val bumpedUpdates = (List(0, 42, 147, 153, 654, 834, 2301).map(touchUpdate(_, true)) ++ List(1, 42, 150, 200).map(touchUpdate(_, false))).toSet + val bumpedUpdates = (List(0, 42, 147, 153, 654, 834, 4301).map(touchUpdate(_, true)) ++ List(1, 42, 150, 200).map(touchUpdate(_, false))).toSet bumpedUpdates.foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) assert(BasicSyncResult(ranges = 2, queries = 2, channels = 0, updates = bumpedUpdates.size, nodes = if (requestNodeAnnouncements) 20 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) From fd3682dea1f43ecc6e501f5f1c92a4ba9e741824 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 13:28:05 +0200 Subject: [PATCH 80/86] do nonreg on our own test vectors --- .../wire/LightningMessageCodecsSpec.scala | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) 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 f1e52e7b66..06d48ff3f8 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 @@ -122,7 +122,7 @@ class LightningMessageCodecsSpec extends FunSuite { case class TestItem(msg: Any, hex: String) - test("test vectors") { + test("test vectors for extended channel queries ") { import org.json4s.{CustomSerializer, ShortTypeHints} import org.json4s.JsonAST.JString import org.json4s.jackson.Serialization @@ -150,20 +150,30 @@ 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 = List( - query_channel_range, - query_channel_range_timestamps_checksums, - reply_channel_range, - reply_channel_range_zlib, - reply_channel_range_timestamps_checksums, - reply_channel_range_timestamps_checksums_zlib, - query_short_channel_id, - query_short_channel_id_zlib, - query_short_channel_id_flags, - query_short_channel_id_flags_zlib + + + val refs = Map( + query_channel_range -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000186a0000005dc", + query_channel_range_timestamps_checksums -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", + reply_channel_range -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000b8a06000005dc01001900000000000000008e0000000000003c69000000000045a6c4", + reply_channel_range_zlib -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000006400000006e01001601789c636000833e08659309a65878be010010a9023a", + reply_channel_range_timestamps_checksums -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001900000000000000304300000000000778d6000000000046e1c1011900000282c1000e77c5000778ad00490ab00000b57800955bff031800000457000008ae00000d050000115c000015b300001a0a", + reply_channel_range_timestamps_checksums_zlib -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001801789c63600001036730c55e710d4cbb3d3c080017c303b1012201789c63606a3ac8c0577e9481bd622d8327d7060686ad150c53a3ff0300554707db031800000457000008ae00000d050000115c000015b300001a0a", + query_short_channel_id -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4", + query_short_channel_id_zlib -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001c12b608a69e73e30edbaec0800203b040e", + query_short_channel_id_flags -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060019000000000000002fc80000000000003cc4000000000045a6c4010c01789c6364620100000e0008", + query_short_channel_id_flags_zlib -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001f30a30c5b0cd144cb92e3b020017c6034a010c01789c6364620100000e0008" ) - class EncodingTypeSerializer extends CustomSerializer[EncodingType](format => ( { + val items = refs.map { case (obj, refbin) => + val bin = lightningMessageCodec.encode(obj).require + assert(refbin.bits === bin) + TestItem(obj, bin.toHex) + } + + // NB: uncomment this to update the test vectors + + /*class EncodingTypeSerializer extends CustomSerializer[EncodingType](format => ( { null }, { case EncodingType.UNCOMPRESSED => JString("UNCOMPRESSED") @@ -207,12 +217,8 @@ class LightningMessageCodecsSpec extends FunSuite { classOf[ReplyChannelRange], classOf[QueryShortChannelIds])) - val items = refs.map { obj => - val bin = lightningMessageCodec.encode(obj).require - TestItem(obj, bin.toHex) - } val json = Serialization.writePretty(items) - println(json) + println(json)*/ } test("decode channel_update with htlc_maximum_msat") { From 11c12be02ef7735c2f734ad262e56ba12a400c75 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 13:54:42 +0200 Subject: [PATCH 81/86] fixes as per @t-bast review --- .../src/main/scala/fr/acinq/eclair/router/Router.scala | 10 ++++------ .../src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) 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 0c8d125124..3e8cb3da32 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 @@ -72,8 +72,8 @@ case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, ca def updateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) def updateFor(n: PublicKey): Option[ChannelUpdate] = if (n == ann.nodeId1) update_1_opt else if (n == ann.nodeId2) update_2_opt else throw new IllegalArgumentException("this node is unrelated to this channel") } -case class PrivateChannel(nodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate])(implicit nodeParams: NodeParams) { - val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeParams.nodeId, nodeId)) (nodeParams.nodeId, nodeId) else (nodeId, nodeParams.nodeId) +case class PrivateChannel(localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate]) { + val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) nodeId1 else nodeId2 def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt def updateSameSideAs(u: ChannelUpdate): PrivateChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) @@ -135,8 +135,6 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ import Router._ - implicit val np = nodeParams - import ExecutionContext.Implicits.global // we pass these to helpers classes so that they have the logging context @@ -212,7 +210,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ // channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations) // let's create a corresponding private channel and process the channel_update log.info("adding unannounced local channel to remote={} shortChannelId={}", remoteNodeId, shortChannelId) - stay using handle(u, self, d.copy(privateChannels = d.privateChannels + (shortChannelId -> PrivateChannel(remoteNodeId, None, None)))) + stay using handle(u, self, d.copy(privateChannels = d.privateChannels + (shortChannelId -> PrivateChannel(nodeParams.nodeId, remoteNodeId, None, None)))) } } @@ -584,7 +582,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ )) .toList val (sync1, replynow_opt) = addToSync(d.sync, remoteNodeId, replies) - // we only send a rely right away if there were no pending requests + // we only send a reply right away if there were no pending requests replynow_opt.foreach(transport ! _) context.system.eventStream.publish(syncProgress(sync1)) stay using d.copy(sync = sync1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala index b87b1ebcb6..54cb65c948 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala @@ -51,8 +51,8 @@ case class TlvStream[T <: Tlv](records: Traversable[T], unknown: Traversable[Gen /** * * @tparam R input type parameter, must be a subtype of the main TLV type - * @return the TLV record of of type that matches the input type parameter if any (there can be at most one, since BOLTs specify - * that TLV records are supposed to be unique + * @return the TLV record of type that matches the input type parameter if any (there can be at most one, since BOLTs specify + * that TLV records are supposed to be unique) */ def get[R <: T : ClassTag]: Option[R] = records.collectFirst { case r: R => r } } From b0c45b6947687f7f81a86fe6d963fece67ab27e8 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 14:02:27 +0200 Subject: [PATCH 82/86] fixed previous merge --- .../fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 1eb566644e..ca066c3e71 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -96,19 +96,19 @@ class ChannelRangeQueriesSpec extends FunSuite { import fr.acinq.eclair.wire.QueryShortChannelIdsTlv.QueryFlagType._ - assert(Router.getChannelDigestInfo(channels)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 1697591108L))) + assert(Router.getChannelDigestInfo(channels)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 3692323747L))) // no extended info but we know the channel: we ask for the updates assert(Router.computeFlag(channels)(ab.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) assert(Router.computeFlag(channels)(ab.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // same checksums, newer timestamps: we don't ask anything - assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 1697591108L)), true) === 0) + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), true) === 0) // different checksums, newer timestamps: we ask for the updates - assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 1697591108L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2| INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // different checksums, older timestamps: we don't ask anything - assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 1697591108L)), true) === 0) + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), true) === 0) assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), true) === 0) assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0) From cd902879396218ea5f9e992327a1756a353e7c43 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 14:03:57 +0200 Subject: [PATCH 83/86] increased test timeout --- .../src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 13a8631f17..732f76487b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -168,7 +168,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { } awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) assert(BasicSyncResult(ranges = 2, queries = 46, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) } From 310b9ba9c8e4dd6d3299b2de26c010c82fbcd675 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 14:20:17 +0200 Subject: [PATCH 84/86] inceased more timeouts --- .../fr/acinq/eclair/router/RoutingSyncSpec.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 732f76487b..f0f7cfa1ec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -166,7 +166,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) + awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 60 seconds) assert(BasicSyncResult(ranges = 2, queries = 46, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) @@ -196,7 +196,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 40) assert(BasicSyncResult(ranges = 1, queries = 1, channels = 40, updates = 40, nodes = if (requestNodeAnnouncements) 80 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) @@ -207,7 +207,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { } awaitCond(bob.stateData.channels.size === 40 && bob.stateData.updates.size === 80) assert(BasicSyncResult(ranges = 1, queries = 1, channels = 0, updates = 40, nodes = if (requestNodeAnnouncements) 80 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) // add everything (duplicates will be ignored) @@ -219,9 +219,9 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 20 seconds) + awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && bob.stateData.updates.size === 2 * fakeRoutingInfo.size, max = 60 seconds) assert(BasicSyncResult(ranges = 2, queries = 46, channels = fakeRoutingInfo.size - 40, updates = 2 * (fakeRoutingInfo.size - 40), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 40) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) // bump random channel_updates @@ -233,7 +233,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val bumpedUpdates = (List(0, 42, 147, 153, 654, 834, 4301).map(touchUpdate(_, true)) ++ List(1, 42, 150, 200).map(touchUpdate(_, false))).toSet bumpedUpdates.foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) assert(BasicSyncResult(ranges = 2, queries = 2, channels = 0, updates = bumpedUpdates.size, nodes = if (requestNodeAnnouncements) 20 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 20 seconds) + awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) awaitCond(alice.stateData.updates === bob.stateData.updates) if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) } From b1039d20cb56d212df48aba896e18ece64eb1017 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 15:20:58 +0200 Subject: [PATCH 85/86] naming as per @sstone's remarks --- .../scala/fr/acinq/eclair/router/Router.scala | 25 +++++++++---------- .../eclair/integration/IntegrationSpec.scala | 8 +++--- 2 files changed, 17 insertions(+), 16 deletions(-) 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 3e8cb3da32..2bebea38fe 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 @@ -68,15 +68,14 @@ case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, ca update_1_opt.foreach(u => assert(Announcements.isNode1(u.channelFlags))) update_2_opt.foreach(u => assert(!Announcements.isNode1(u.channelFlags))) def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) ann.nodeId1 else ann.nodeId2 - def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt - def updateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) - def updateFor(n: PublicKey): Option[ChannelUpdate] = if (n == ann.nodeId1) update_1_opt else if (n == ann.nodeId2) update_2_opt else throw new IllegalArgumentException("this node is unrelated to this channel") + def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt + def updateChannelUpdateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) } case class PrivateChannel(localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate]) { val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) nodeId1 else nodeId2 - def getSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt - def updateSameSideAs(u: ChannelUpdate): PrivateChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) + def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt + def updateChannelUpdateSameSideAs(u: ChannelUpdate): PrivateChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) } case class AssistedChannel(extraHop: ExtraHop, nextNodeId: PublicKey) @@ -689,14 +688,14 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ } else if (isStale(u)) { log.debug("ignoring {} (stale)", u) d - } else if (pc.getSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (duplicate)", u) d } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) origin ! InvalidSignature(u) d - } else if (pc.getSameSideAs(u).isDefined) { + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) db.updateChannel(u) @@ -705,14 +704,14 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ case true => d.graph.removeEdge(desc).addEdge(desc, u) case false => d.graph.removeEdge(desc) // if the channel is now disabled, we remove it from the graph } - d.copy(channels = d.channels + (u.shortChannelId -> pc.updateSameSideAs(u)), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) + d.copy(channels = d.channels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) db.updateChannel(u) // we also need to update the graph val graph1 = d.graph.addEdge(desc, u) - d.copy(channels = d.channels + (u.shortChannelId -> pc.updateSameSideAs(u)), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) + d.copy(channels = d.channels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin))), graph = graph1) } } else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) { // channel is currently being validated @@ -731,25 +730,25 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ if (isStale(u)) { log.debug("ignoring {} (stale)", u) d - } else if (pc.getSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (already know same or newer)", u) d } else if (!Announcements.checkSig(u, desc.a)) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) origin ! InvalidSignature(u) d - } else if (pc.getSameSideAs(u).isDefined) { + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph val graph1 = d.graph.removeEdge(desc).addEdge(desc, u) - d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateSameSideAs(u)), graph = graph1) + d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), graph = graph1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph val graph1 = d.graph.addEdge(desc, u) - d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateSameSideAs(u)), graph = graph1) + d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), graph = graph1) } } else if (db.isPruned(u.shortChannelId) && !isStale(u)) { // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case 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 0464d921ee..cf141f8467 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 @@ -23,7 +23,7 @@ import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{TestKit, TestProbe} import com.google.common.net.HostAndPort import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient @@ -295,11 +295,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val ps = sender.expectMsgType[PaymentSucceeded](5 seconds) assert(ps.id == paymentId) + def updateFor(n: PublicKey, pc: PublicChannel): Option[ChannelUpdate] = if (n == pc.ann.nodeId1) pc.update_1_opt else if (n == pc.ann.nodeId2) pc.update_2_opt else throw new IllegalArgumentException("this node is unrelated to this channel") + awaitCond({ // in the meantime, the router will have updated its state sender.send(nodes("A").router, 'channelsMap) // we then put everything back like before by asking B to refresh its channel update (this will override the one we created) - val u_opt = sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId).updateFor(nodes("B").nodeParams.nodeId) + val u_opt = updateFor(nodes("B").nodeParams.nodeId, sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId))) u_opt == Some(channelUpdateBC) }, max = 30 seconds, interval = 1 seconds) @@ -314,7 +316,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(channelUpdateBC_new.cltvExpiryDelta == nodes("B").nodeParams.expiryDeltaBlocks) awaitCond({ sender.send(nodes("A").router, 'channelsMap) - val u = sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId).updateFor(nodes("B").nodeParams.nodeId).get + val u = updateFor(nodes("B").nodeParams.nodeId, sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId)).get u.cltvExpiryDelta == nodes("B").nodeParams.expiryDeltaBlocks }, max = 30 seconds, interval = 1 second) } From 1a3804e2370122edaa2e6ed41e587503ab88d16c Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 28 Aug 2019 16:24:48 +0200 Subject: [PATCH 86/86] fixed merge error in 14e720336c8f18a264941015a453f3efeb063e31 --- .../src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 7421d21ed7..b06ff1866c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -145,7 +145,7 @@ class SqliteNetworkDbSpec extends FunSuite { channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, None, None))) val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, a, b.publicKey, ShortChannelId(42), CltvExpiryDelta(5), MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) - val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, a.publicKey, ShortChannelId(43), CltvExpiryDelta(5), MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) + val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, a.publicKey, ShortChannelId(42), CltvExpiryDelta(5), MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, c.publicKey, ShortChannelId(44), CltvExpiryDelta(5), MilliSatoshi(7000000), MilliSatoshi(50000), 100, MilliSatoshi(500000000L), true) db.updateChannel(channel_update_1)