From b56f0359fe6aa45674cf716190aacad9227364f4 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 19 Sep 2018 16:42:09 +0200 Subject: [PATCH 01/31] Fixed regression in rebroadcast (#713) Fixed regression caused by 2c1811d: we now don't force sending a channel_update at the same time with channel_announcement. This greatly simplifies the rebroadcast logic, and is what caused the integration test to fail. Added proper test on Peer, testing the actor, not only static methods. --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 63 ++++------ .../electrum/ElectrumWalletBasicSpec.scala | 5 +- .../electrum/ElectrumWatcherSpec.scala | 2 - .../electrum/ElectrumxService.scala | 4 +- .../fee/EarnDotComFeeProviderSpec.scala | 5 +- .../fr/acinq/eclair/crypto/NoiseDemo.scala | 1 - .../eclair/integration/IntegrationSpec.scala | 9 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 94 ++++++++------- .../eclair/transactions/TestVectorsSpec.scala | 109 +++++++++--------- .../transactions/TransactionsSpec.scala | 5 +- 10 files changed, 145 insertions(+), 152 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 cd4a3d378..685410d96 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 @@ -249,34 +249,39 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay case Event(rebroadcast: Rebroadcast, ConnectedData(_, transport, _, _, maybeGossipTimestampFilter, _)) => - val (channels1, updates1, nodes1) = Peer.filterGossipMessages(rebroadcast, self, maybeGossipTimestampFilter) /** * Send and count in a single iteration */ - def sendAndCount(msgs: Iterable[RoutingMessage]): Int = msgs.foldLeft(0) { - case (count, msg) => + def sendAndCount(msgs: Map[_ <: RoutingMessage, Set[ActorRef]]): Int = msgs.foldLeft(0) { + case (count, (_, origins)) if origins.contains(self) => + // the announcement came from this peer, we don't send it back + count + case (count, (msg: HasTimestamp, _)) if !timestampInRange(msg, maybeGossipTimestampFilter) => + // the peer has set up a filter on timestamp and this message is out of range + count + case (count, (msg, _)) => transport ! msg count + 1 } - val channelsSent = sendAndCount(channels1) - val updatesSent = sendAndCount(updates1) - val nodesSent = sendAndCount(nodes1) + val channelsSent = sendAndCount(rebroadcast.channels) + val updatesSent = sendAndCount(rebroadcast.updates) + val nodesSent = sendAndCount(rebroadcast.nodes) if (channelsSent > 0 || updatesSent > 0 || nodesSent > 0) { - log.debug(s"sent announcements to {}: channels={} updates={} nodes={}", remoteNodeId, channelsSent, updatesSent, nodesSent) + log.info(s"sent announcements to {}: channels={} updates={} nodes={}", remoteNodeId, channelsSent, updatesSent, nodesSent) } stay case Event(msg: GossipTimestampFilter, data: ConnectedData) => - // special case: time range filters are peer specific and must not be sent to - // the router + // special case: time range filters are peer specific and must not be sent to the router sender ! TransportHandler.ReadAck(msg) if (msg.chainHash != nodeParams.chainHash) { log.warning("received gossip_timestamp_range message for chain {}, we're on {}", msg.chainHash, nodeParams.chainHash) stay } else { + log.info(s"setting up gossipTimestampFilter=$msg") // update their timestamp filter stay using data.copy(gossipTimestampFilter = Some(msg)) } @@ -500,42 +505,18 @@ object Peer { } /** - * filter out gossip messages using the provided origin and optional timestamp range + * Peer may want to filter announcements based on timestamp * - * @param rebroadcast rebroadcast message - * @param self messages which have been sent by `self` will be filtered out - * @param gossipTimestampFilter optional gossip timestamp range - * @return a filtered (channel announcements, channel updates, node announcements) tuple + * @param gossipTimestampFilter_opt optional gossip timestamp range + * @return + * - true if the msg's timestamp is in the requested range, or if there is no filtering + * - false otherwise */ - def filterGossipMessages(rebroadcast: Rebroadcast, self: ActorRef, gossipTimestampFilter: Option[GossipTimestampFilter]): (Iterable[ChannelAnnouncement], Iterable[ChannelUpdate], Iterable[NodeAnnouncement]) = { - + def timestampInRange(msg: HasTimestamp, gossipTimestampFilter_opt: Option[GossipTimestampFilter]): Boolean = { // check if this message has a timestamp that matches our timestamp filter - def checkTimestamp(routingMessage: RoutingMessage): Boolean = gossipTimestampFilter match { + gossipTimestampFilter_opt match { case None => true // no filtering - case Some(GossipTimestampFilter(_, firstTimestamp, timestampRange)) => routingMessage match { - case hts: HasTimestamp => hts.timestamp >= firstTimestamp && hts.timestamp <= firstTimestamp + timestampRange - case _ => true - } + case Some(GossipTimestampFilter(_, firstTimestamp, timestampRange)) => msg.timestamp >= firstTimestamp && msg.timestamp <= firstTimestamp + timestampRange } - - // we filter out updates against their timestamp filter, and build a list of all channel ids for which we have an update - val (updates1, shortChannelIds) = rebroadcast.updates.foldLeft((Seq.empty[ChannelUpdate], Set.empty[ShortChannelId])) { - case ((channelUpdates, shortChannelIds), (a, origins)) if !origins.contains(self) && checkTimestamp(a) => (a +: channelUpdates, shortChannelIds + a.shortChannelId) - case ((channelUpdates, shortChannelIds), (a, origins)) => (channelUpdates, shortChannelIds) - } - - // we filter out channels for which we don't have an update - val channels1 = rebroadcast.channels.foldLeft((Seq.empty[ChannelAnnouncement])) { - case (channelAnnouncements, (a, origins)) if !origins.contains(self) && shortChannelIds.contains(a.shortChannelId) => a +: channelAnnouncements - case (channelAnnouncements, (a, _)) => channelAnnouncements - } - - // we filter out nodes against their timestamp filter - // TODO: we do * not * filter out nodes for which matching channel announcements were pruned above. - // Our rebroadcast message may sometimes include "orphan" nodes without matching channel announcements, because of - // the way announcements are handled in the router - val nodes1 = rebroadcast.nodes.collect { case (a, origins) if !origins.contains(self) && checkTimestamp(a) => a } - - (channels1, updates1, nodes1) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala index 28d6920e0..64b1d07e2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala @@ -20,6 +20,7 @@ import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKey} import fr.acinq.bitcoin._ import fr.acinq.eclair.transactions.Transactions +import grizzled.slf4j.Logging import org.junit.runner.RunWith import org.scalatest.FunSuite import org.scalatest.junit.JUnitRunner @@ -27,7 +28,7 @@ import org.scalatest.junit.JUnitRunner import scala.util.{Failure, Random, Success, Try} @RunWith(classOf[JUnitRunner]) -class ElectrumWalletBasicSpec extends FunSuite { +class ElectrumWalletBasicSpec extends FunSuite with Logging { import ElectrumWallet._ import ElectrumWalletBasicSpec._ @@ -197,7 +198,7 @@ class ElectrumWalletBasicSpec extends FunSuite { Try(state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true)) match { case Success((state2, tx1, fee1)) => () case Failure(cause) if cause.getMessage != null && cause.getMessage.contains("insufficient funds") => () - case Failure(cause) => println(s"unexpected $cause") + case Failure(cause) => logger.error(s"unexpected $cause") } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala index 24c725973..e520cac0d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala @@ -59,7 +59,6 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike probe.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(address) = probe.expectMsgType[JValue] - println(address) probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) val JString(txid) = probe.expectMsgType[JValue](3000 seconds) @@ -84,7 +83,6 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike probe.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(address) = probe.expectMsgType[JValue] - println(address) probe.send(bitcoincli, BitcoinReq("dumpprivkey", address)) val JString(wif) = probe.expectMsgType[JValue] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala index 6a3339222..01094588e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala @@ -30,14 +30,14 @@ trait ElectrumxService extends DockerTestKit { DockerContainer("lukechilds/electrumx") .withNetworkMode("host") .withEnv("DAEMON_URL=http://foo:bar@localhost:28332", "COIN=BitcoinSegwit", "NET=regtest") - .withLogLineReceiver(LogLineReceiver(true, println)) + //.withLogLineReceiver(LogLineReceiver(true, println)) } else { // on windows or oxs, host mode is not available, but from docker 18.03 on host.docker.internal can be used instead // host.docker.internal is not (yet ?) available on linux though DockerContainer("lukechilds/electrumx") .withPorts(50001 -> Some(50001)) .withEnv("DAEMON_URL=http://foo:bar@host.docker.internal:28332", "COIN=BitcoinSegwit", "NET=regtest", "TCP_PORT=50001") - .withLogLineReceiver(LogLineReceiver(true, println)) + //.withLogLineReceiver(LogLineReceiver(true, println)) } override def dockerContainers: List[DockerContainer] = electrumxContainer :: super.dockerContainers diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala index 3afb0fcd7..d2fc8a2ad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.blockchain.fee import akka.actor.ActorSystem import akka.util.Timeout +import grizzled.slf4j.Logging import org.json4s.DefaultFormats import org.junit.runner.RunWith import org.scalatest.FunSuite @@ -29,7 +30,7 @@ import scala.concurrent.Await * Created by PM on 27/01/2017. */ @RunWith(classOf[JUnitRunner]) -class EarnDotComFeeProviderSpec extends FunSuite { +class EarnDotComFeeProviderSpec extends FunSuite with Logging { import EarnDotComFeeProvider._ import org.json4s.jackson.JsonMethods.parse @@ -74,7 +75,7 @@ class EarnDotComFeeProviderSpec extends FunSuite { implicit val system = ActorSystem() implicit val timeout = Timeout(30 seconds) val provider = new EarnDotComFeeProvider() - println("earn.com livenet fees: " + Await.result(provider.getFeerates, 10 seconds)) + logger.info("earn.com livenet fees: " + Await.result(provider.getFeerates, 10 seconds)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala index 8abbb9ad7..b59c1f916 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala @@ -100,7 +100,6 @@ object NoiseDemo extends App { def receive = { case message: BinaryData => - println(s"received ${new String(message)}") sender ! BinaryData("response to ".getBytes() ++ message) count = count + 1 if (count == 5) context stop self 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 8ca63a208..4552dd4fb 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 @@ -260,6 +260,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("A").router, 'updatesMap) assert(sender.expectMsgType[Map[ChannelDesc, ChannelUpdate]].apply(ChannelDesc(channelUpdateBC.shortChannelId, nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)) === channelUpdateBC) // we then put everything back like before by asking B to refresh its channel update (this will override the one we created) + // first let's wait 3 seconds to make sure the timestamp of the new channel_update will be strictly greater than the former + sender.expectNoMsg(3 seconds) sender.send(nodes("B").register, ForwardShortId(shortIdBC, TickRefreshChannelUpdate)) sender.send(nodes("B").register, ForwardShortId(shortIdBC, CMD_GETINFO)) val channelUpdateBC_new = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].channelUpdate @@ -685,6 +687,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sigListener.expectMsgType[ChannelSignatureReceived] sigListener.expectMsgType[ChannelSignatureReceived] sender.expectMsgType[PaymentSucceeded] + // 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")) @@ -692,6 +695,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sendReq = SendPayment(amountMsat, pr.paymentHash, pr.nodeId) sender.send(paymentInitiator, sendReq) } + val buffer = TestProbe() send(100000000, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending forwardHandlerF.expectMsgType[UpdateAddHtlc] @@ -797,12 +801,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // then we make the announcements val announcements = channels.map(c => AnnouncementsBatchValidationSpec.makeChannelAnnouncement(c)) announcements.foreach(ann => nodes("A").router ! PeerRoutingMessage(sender.ref, remoteNodeId, ann)) - // we need to send channel_update otherwise router won't validate the channels - val updates = channels.zip(announcements).map(x => AnnouncementsBatchValidationSpec.makeChannelUpdate(x._1, x._2.shortChannelId)) - updates.foreach(update => nodes("A").router ! PeerRoutingMessage(sender.ref, remoteNodeId, update)) awaitCond({ sender.send(nodes("D").router, 'channels) - sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared + sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-5} have disappeared }, max = 120 seconds, interval = 1 second) } 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 b56d3d416..55a9cf9b4 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,20 +4,19 @@ import java.net.InetSocketAddress import akka.actor.ActorRef import akka.testkit.TestProbe -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.Crypto.PublicKey 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.{ShortChannelId, TestkitBaseClass, randomKey, wire} +import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} +import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, wire} import org.junit.runner.RunWith import org.scalatest.Outcome import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ -import scala.util.Random @RunWith(classOf[JUnitRunner]) class PeerSpec extends TestkitBaseClass { @@ -27,42 +26,9 @@ class PeerSpec extends TestkitBaseClass { val updates = (fakeRoutingInfo.map(_._2) ++ fakeRoutingInfo.map(_._3)).toList val nodes = (fakeRoutingInfo.map(_._4) ++ fakeRoutingInfo.map(_._5)).toList - override type FixtureParam = TestProbe + override type FixtureParam = Tuple8[PublicKey, TestProbe, TestProbe, TestProbe, TestProbe, TestProbe, TestProbe, ActorRef] override protected def withFixture(test: OneArgTest): Outcome = { - val probe = TestProbe() - test(probe) - } - - test("filter gossip message (no filtering)") { probe => - val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap) - val (channels1, updates1, nodes1) = Peer.filterGossipMessages(rebroadcast, probe.ref, None) - assert(channels1.toSet == channels.toSet) - assert(updates1.toSet == updates.toSet) - assert(nodes1.toSet == nodes.toSet) - } - - test("filter gossip message (filtered by origin)") { probe => - val rebroadcast = Rebroadcast( - channels.map(_ -> Set.empty[ActorRef]).toMap + (channels(5) -> Set(probe.ref)), - updates.map(_ -> Set.empty[ActorRef]).toMap + (updates(6) -> Set(probe.ref)) + (updates(10) -> Set(probe.ref)), - nodes.map(_ -> Set.empty[ActorRef]).toMap + (nodes(4) -> Set(probe.ref))) - val (channels1, updates1, nodes1) = Peer.filterGossipMessages(rebroadcast, probe.ref, None) - assert(channels1.toSet == channels.toSet - channels(5)) - assert(updates1.toSet == updates.toSet - updates(6) - updates(10)) - assert(nodes1.toSet == nodes.toSet - nodes(4)) - } - - test("filter gossip message (filtered by timestamp)") { probe => - val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap) - val timestamps = updates.map(_.timestamp).sorted.drop(10).take(20) - val (channels1, updates1, nodes1) = Peer.filterGossipMessages(rebroadcast, probe.ref, Some(wire.GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, timestamps.head, timestamps.last - timestamps.head))) - assert(updates1.toSet == updates.filter(u => timestamps.contains(u.timestamp)).toSet) - assert(nodes1.toSet == nodes.filter(u => timestamps.contains(u.timestamp)).toSet) - assert(channels1.toSet == channels.filter(ca => updates1.map(_.shortChannelId).toSet.contains(ca.shortChannelId)).toSet) - } - - test("react to peer's bad behavior") { probe => val authenticator = TestProbe() val watcher = TestProbe() val router = TestProbe() @@ -72,8 +38,12 @@ class PeerSpec extends TestkitBaseClass { val wallet: EclairWallet = null // unused val remoteNodeId = Bob.nodeParams.nodeId val peer = system.actorOf(Peer.props(Alice.nodeParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) + test((remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer)) + } + def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef): Unit = { // let's simulate a connection + val probe = TestProbe() probe.send(peer, Peer.Init(None, Set.empty)) authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, InetSocketAddress.createUnresolved("foo.bar", 42000), false, None)) transport.expectMsgType[TransportHandler.Listener] @@ -83,10 +53,52 @@ class PeerSpec extends TestkitBaseClass { router.expectNoMsg(1 second) // bob's features require no sync probe.send(peer, Peer.GetPeerInfo) assert(probe.expectMsgType[Peer.PeerInfo].state == "CONNECTED") + } - val channels = for (_ <- 0 until 12) yield RouteCalculationSpec.makeChannel(Random.nextInt(10000000), randomKey.publicKey, randomKey.publicKey) - val updates = for (_ <- 0 until 20) yield RouteCalculationSpec.makeUpdate(Random.nextInt(10000000), randomKey.publicKey, randomKey.publicKey, Random.nextInt(1000), Random.nextInt(1000))._2 - val query = wire.QueryShortChannelIds(Block.RegtestGenesisBlock.hash, ChannelRangeQueries.encodeShortChannelIdsSingle(Seq(ShortChannelId(42000)), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + test("filter gossip message (no filtering)") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + val probe = TestProbe() + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap) + probe.send(peer, rebroadcast) + channels.foreach(transport.expectMsg(_)) + updates.foreach(transport.expectMsg(_)) + nodes.foreach(transport.expectMsg(_)) + } + + test("filter gossip message (filtered by origin)") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + val probe = TestProbe() + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + val rebroadcast = Rebroadcast( + channels.map(_ -> Set.empty[ActorRef]).toMap + (channels(5) -> Set(peer)), + updates.map(_ -> Set.empty[ActorRef]).toMap + (updates(6) -> Set(peer)) + (updates(10) -> Set(peer)), + nodes.map(_ -> Set.empty[ActorRef]).toMap + (nodes(4) -> Set(peer))) + probe.send(peer, rebroadcast) + // peer won't send out announcements that came from itself + (channels.toSet - channels(5)).foreach(transport.expectMsg(_)) + (updates.toSet - updates(6) - updates(10)).foreach(transport.expectMsg(_)) + (nodes.toSet - nodes(4)).foreach(transport.expectMsg(_)) + } + + test("filter gossip message (filtered by timestamp)") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + val probe = TestProbe() + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap) + val timestamps = updates.map(_.timestamp).sorted.drop(10).take(20) + val filter = wire.GossipTimestampFilter(Alice.nodeParams.chainHash, timestamps.head, timestamps.last - timestamps.head) + probe.send(peer, filter) + probe.send(peer, rebroadcast) + // peer doesn't filter channel announcements + channels.foreach(transport.expectMsg(_)) + // but it will only send updates and node annoucements matching the filter + updates.filter(u => timestamps.contains(u.timestamp)).foreach(transport.expectMsg(_)) + nodes.filter(u => timestamps.contains(u.timestamp)).foreach(transport.expectMsg(_)) + } + + test("react to peer's bad behavior") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + 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)) // make sure that routing messages go through for (ann <- channels ++ updates) { 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 e1458713c..1bf7e63e5 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 @@ -22,11 +22,12 @@ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} import fr.acinq.eclair.wire.UpdateAddHtlc +import grizzled.slf4j.Logging import org.scalatest.FunSuite import scala.io.Source -class TestVectorsSpec extends FunSuite { +class TestVectorsSpec extends FunSuite with Logging { val results = collection.mutable.HashMap.empty[String, Map[String, String]] val current = collection.mutable.HashMap.empty[String, String] @@ -120,7 +121,7 @@ class TestVectorsSpec extends FunSuite { val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingAmount = fundingTx.txOut(0).amount - println(s"# funding-tx: $fundingTx}") + logger.info(s"# funding-tx: $fundingTx}") val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey) @@ -128,18 +129,18 @@ class TestVectorsSpec extends FunSuite { val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, true, Local.payment_basepoint, Remote.payment_basepoint) assert(obscured_tx_number === (0x2bb038521914L ^ 42L)) - println(s"local_payment_basepoint: ${Local.payment_basepoint}") - println(s"remote_payment_basepoint: ${Remote.payment_basepoint}") - println(s"local_funding_privkey: ${Local.funding_privkey}") - println(s"local_funding_pubkey: ${Local.funding_pubkey}") - println(s"remote_funding_privkey: ${Remote.funding_privkey}") - println(s"remote_funding_pubkey: ${Remote.funding_pubkey}") - println(s"local_secretkey: ${Local.payment_privkey}") - println(s"localkey: ${Local.payment_privkey.publicKey}") - println(s"remotekey: ${Remote.payment_privkey.publicKey}") - println(s"local_delayedkey: ${Local.delayed_payment_privkey.publicKey}") - println(s"local_revocation_key: ${Local.revocation_pubkey}") - println(s"# funding wscript = ${commitmentInput.redeemScript}") + logger.info(s"local_payment_basepoint: ${Local.payment_basepoint}") + logger.info(s"remote_payment_basepoint: ${Remote.payment_basepoint}") + logger.info(s"local_funding_privkey: ${Local.funding_privkey}") + logger.info(s"local_funding_pubkey: ${Local.funding_pubkey}") + logger.info(s"remote_funding_privkey: ${Remote.funding_privkey}") + logger.info(s"remote_funding_pubkey: ${Remote.funding_pubkey}") + logger.info(s"local_secretkey: ${Local.payment_privkey}") + logger.info(s"localkey: ${Local.payment_privkey.publicKey}") + logger.info(s"remotekey: ${Remote.payment_privkey.publicKey}") + logger.info(s"local_delayedkey: ${Local.delayed_payment_privkey.publicKey}") + logger.info(s"local_revocation_key: ${Local.revocation_pubkey}") + logger.info(s"# funding wscript = ${commitmentInput.redeemScript}") assert(commitmentInput.redeemScript == BinaryData("5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae")) val paymentPreimages = Seq( @@ -168,17 +169,16 @@ class TestVectorsSpec extends FunSuite { } for (i <- 0 until htlcs.length) { - println(s"htlc $i direction: ${dir2string(htlcs(i).direction)}") - println(s"htlc $i amount_msat: ${htlcs(i).add.amountMsat}") - println(s"htlc $i expiry: ${htlcs(i).add.expiry}") - println(s"htlc $i payment_preimage: ${paymentPreimages(i)}") + logger.info(s"htlc $i direction: ${dir2string(htlcs(i).direction)}") + logger.info(s"htlc $i amount_msat: ${htlcs(i).add.amountMsat}") + logger.info(s"htlc $i expiry: ${htlcs(i).add.expiry}") + logger.info(s"htlc $i payment_preimage: ${paymentPreimages(i)}") } - println() def run(spec: CommitmentSpec) = { - println(s"to_local_msat: ${spec.toLocalMsat}") - println(s"to_remote_msat: ${spec.toRemoteMsat}") - println(s"local_feerate_per_kw: ${spec.feeratePerKw}") + logger.info(s"to_local_msat: ${spec.toLocalMsat}") + logger.info(s"to_remote_msat: ${spec.toRemoteMsat}") + logger.info(s"local_feerate_per_kw: ${spec.feeratePerKw}") val commitTx = { val tx = Transactions.makeCommitTx( @@ -197,16 +197,16 @@ class TestVectorsSpec extends FunSuite { } val baseFee = Transactions.commitTxFee(Local.dustLimit, spec) - println(s"# base commitment transaction fee = ${baseFee.toLong}") + logger.info(s"# base commitment transaction fee = ${baseFee.toLong}") val actualFee = fundingAmount - commitTx.tx.txOut.map(_.amount).sum - println(s"# actual commitment transaction fee = ${actualFee.toLong}") + logger.info(s"# actual commitment transaction fee = ${actualFee.toLong}") commitTx.tx.txOut.map(txOut => { txOut.publicKeyScript.length match { - case 22 => println(s"# to-remote amount ${txOut.amount.toLong} P2WPKH(${Remote.payment_privkey.publicKey})") + case 22 => logger.info(s"# to-remote amount ${txOut.amount.toLong} P2WPKH(${Remote.payment_privkey.publicKey})") case 34 => val index = htlcScripts.indexWhere(s => Script.write(Script.pay2wsh(s)) == txOut.publicKeyScript) - if (index == -1) println(s"# to-local amount ${txOut.amount.toLong} wscript ${Script.write(Scripts.toLocalDelayed(Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey))}") - else println(s"# HTLC ${if (htlcs(index).direction == OUT) "offered" else "received"} amount ${txOut.amount.toLong} wscript ${Script.write(htlcScripts(index))}") + if (index == -1) logger.info(s"# to-local amount ${txOut.amount.toLong} wscript ${Script.write(Scripts.toLocalDelayed(Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey))}") + else logger.info(s"# HTLC ${if (htlcs(index).direction == OUT) "offered" else "received"} amount ${txOut.amount.toLong} wscript ${Script.write(htlcScripts(index))}") } }) @@ -221,14 +221,14 @@ class TestVectorsSpec extends FunSuite { spec) val local_sig = Transactions.sign(tx, Local.funding_privkey) - println(s"# local_signature = ${toHexString(local_sig.dropRight(1))}") + logger.info(s"# local_signature = ${toHexString(local_sig.dropRight(1))}") val remote_sig = Transactions.sign(tx, Remote.funding_privkey) - println(s"remote_signature: ${toHexString(remote_sig.dropRight(1))}") + logger.info(s"remote_signature: ${toHexString(remote_sig.dropRight(1))}") } assert(Transactions.getCommitTxNumber(commitTx.tx, true, Local.payment_basepoint, Remote.payment_basepoint) === Local.commitTxNumber) Transaction.correctlySpends(commitTx.tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - println(s"output commit_tx: ${commitTx.tx}") + logger.info(s"output commit_tx: ${commitTx.tx}") val (unsignedHtlcTimeoutTxs, unsignedHtlcSuccessTxs) = Transactions.makeHtlcTxs( commitTx.tx, @@ -238,7 +238,7 @@ class TestVectorsSpec extends FunSuite { Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key spec) - println(s"num_htlcs: ${(unsignedHtlcTimeoutTxs ++ unsignedHtlcSuccessTxs).length}") + logger.info(s"num_htlcs: ${(unsignedHtlcTimeoutTxs ++ unsignedHtlcSuccessTxs).length}") val htlcTxs: Seq[TransactionWithInputInfo] = (unsignedHtlcTimeoutTxs ++ unsignedHtlcSuccessTxs).sortBy(_.input.outPoint.index) @@ -246,13 +246,13 @@ class TestVectorsSpec extends FunSuite { case tx: HtlcSuccessTx => val remoteSig = Transactions.sign(tx, Remote.payment_privkey) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) - println(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") - println(s"remote_htlc_signature: ${toHexString(remoteSig.dropRight(1))}") + logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") + logger.info(s"remote_htlc_signature: ${toHexString(remoteSig.dropRight(1))}") case tx: HtlcTimeoutTx => val remoteSig = Transactions.sign(tx, Remote.payment_privkey) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) - println(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") - println(s"remote_htlc_signature: ${toHexString(remoteSig.dropRight(1))}") + logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") + logger.info(s"remote_htlc_signature: ${toHexString(remoteSig.dropRight(1))}") } val signedTxs = htlcTxs collect { @@ -264,27 +264,26 @@ class TestVectorsSpec extends FunSuite { val tx1 = Transactions.addSigs(tx, localSig, remoteSig, preimage) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) - println(s"# local_signature = ${toHexString(localSig.dropRight(1))}") - println(s"output htlc_success_tx ${htlcIndex}: ${tx1.tx}") + logger.info(s"# local_signature = ${toHexString(localSig.dropRight(1))}") + logger.info(s"output htlc_success_tx ${htlcIndex}: ${tx1.tx}") tx1 case tx: HtlcTimeoutTx => val localSig = Transactions.sign(tx, Local.payment_privkey) val remoteSig = Transactions.sign(tx, Remote.payment_privkey) val tx1 = Transactions.addSigs(tx, localSig, remoteSig) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - println(s"# local_signature = ${toHexString(localSig.dropRight(1))}") + logger.info(s"# local_signature = ${toHexString(localSig.dropRight(1))}") val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) - println(s"output htlc_timeout_tx ${htlcIndex}: ${tx1.tx}") + logger.info(s"output htlc_timeout_tx ${htlcIndex}: ${tx1.tx}") tx1 } - println (commitTx, signedTxs) } test("simple commitment tx with no HTLCs") { val name = "simple commitment tx with no HTLCs" - println(s"name: $name") + logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = 15000, toLocalMsat = 7000000000L, toRemoteMsat = 3000000000L) val (commitTx, htlcTxs) = run(spec) @@ -295,7 +294,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with all 5 htlcs untrimmed (minimum feerate)") { val name = "commitment tx with all 5 htlcs untrimmed (minimum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 0, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) val (commitTx, htlcTxs) = run(spec) @@ -305,7 +304,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 7 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 7 outputs untrimmed (maximum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -319,7 +318,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 6 outputs untrimmed (minimum feerate)") { val name = "commitment tx with 6 outputs untrimmed (minimum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -333,7 +332,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 6 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 6 outputs untrimmed (maximum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -347,7 +346,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 5 outputs untrimmed (minimum feerate)") { val name = "commitment tx with 5 outputs untrimmed (minimum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -361,7 +360,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 5 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 5 outputs untrimmed (maximum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -375,7 +374,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 4 outputs untrimmed (minimum feerate)") { val name = "commitment tx with 4 outputs untrimmed (minimum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -389,7 +388,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 4 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 4 outputs untrimmed (maximum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -403,7 +402,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 3 outputs untrimmed (minimum feerate)") { val name = "commitment tx with 3 outputs untrimmed (minimum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -417,7 +416,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 3 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 3 outputs untrimmed (maximum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -431,7 +430,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 2 outputs untrimmed (minimum feerate)") { val name = "commitment tx with 2 outputs untrimmed (minimum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) @@ -445,7 +444,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 2 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 2 outputs untrimmed (maximum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651180, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) val (commitTx, htlcTxs) = run(spec) @@ -458,7 +457,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with 1 output untrimmed (minimum feerate)") { val name = "commitment tx with 1 output untrimmed (minimum feerate)" - println(s"name: $name") + logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651181, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) val (commitTx, htlcTxs) = run(spec) @@ -471,7 +470,7 @@ class TestVectorsSpec extends FunSuite { test("commitment tx with fee greater than funder amount") { val name = "commitment tx with fee greater than funder amount" - println(s"name: $name") + logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651936, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) val (commitTx, htlcTxs) = run(spec) 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 1b6056cc1..de1e5d6db 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 @@ -25,6 +25,7 @@ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} import fr.acinq.eclair.wire.UpdateAddHtlc +import grizzled.slf4j.Logging import org.junit.runner.RunWith import org.scalatest.FunSuite import org.scalatest.junit.JUnitRunner @@ -36,7 +37,7 @@ import scala.util.{Failure, Random, Success, Try} * Created by PM on 16/12/2016. */ @RunWith(classOf[JUnitRunner]) -class TransactionsSpec extends FunSuite { +class TransactionsSpec extends FunSuite with Logging { test("encode/decode sequence and locktime (one example)") { @@ -360,7 +361,7 @@ class TransactionsSpec extends FunSuite { assert(tests.size === 15, "there were 15 tests at ec99f893f320e8c88f564c1c8566f3454f0f1f5f") tests.foreach(test => { - println(s"running BOLT 2 test: '${test.name}'") + logger.info(s"running BOLT 2 test: '${test.name}'") val fee = commitTxFee(test.dustLimit, test.spec) assert(fee === test.expectedFee) }) From 512c9823b4099353405c483d71ac4d7e36782a37 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Wed, 19 Sep 2018 17:35:21 +0200 Subject: [PATCH 02/31] Routing sync fixes (#712) * 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 | 31 ++++++++------- .../acinq/eclair/router/RoutingSyncSpec.scala | 38 +++++++++++++++++-- 2 files changed, 51 insertions(+), 18 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 92eb9fea1..b99ab813d 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 @@ -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 => @@ -521,7 +524,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 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 a3f1091ce..7c4fdb05f 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 9178b5aa38cd90f92e322b8795e159e06da3447b Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 20 Sep 2018 16:12:38 +0200 Subject: [PATCH 03/31] Ignore 'origin htlc not found' in CLOSING (#708) If we don't have the origin, it means that we already have forwarded the fulfill so that's not a big deal. This can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it. --- .../fr/acinq/eclair/channel/Channel.scala | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index d9eca3f95..dd2886b8c 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 @@ -1141,9 +1141,15 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu log.info(s"processing BITCOIN_OUTPUT_SPENT with txid=${tx.txid} tx=$tx") val extracted = Closing.extractPreimages(d.commitments.localCommit, tx) extracted map { case (htlc, fulfill) => - val origin = d.commitments.originChannels(fulfill.id) - log.warning(s"fulfilling htlc #${fulfill.id} paymentHash=${sha256(fulfill.paymentPreimage)} origin=$origin") - relayer ! ForwardFulfill(fulfill, origin, htlc) + d.commitments.originChannels.get(fulfill.id) match { + case Some(origin) => + log.info(s"fulfilling htlc #${fulfill.id} paymentHash=${sha256(fulfill.paymentPreimage)} origin=$origin") + relayer ! ForwardFulfill(fulfill, origin, htlc) + case None => + // if we don't have the origin, it means that we already have forwarded the fulfill so that's not a big deal. + // this can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it + log.info(s"cannot fulfill htlc #${fulfill.id} paymentHash=${sha256(fulfill.paymentPreimage)} (origin not found)") + } } val revokedCommitPublished1 = d.revokedCommitPublished.map { rev => val (rev1, tx_opt) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx) @@ -1171,16 +1177,26 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu 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)) timedoutHtlcs.foreach { add => - val origin = d.commitments.originChannels(add.id) - log.warning(s"failing htlc #${add.id} paymentHash=${add.paymentHash} origin=$origin: htlc timed out") - relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, HtlcTimedout(d.channelId), origin, None, None)) + d.commitments.originChannels.get(add.id) match { + case Some(origin) => + log.info(s"failing htlc #${add.id} paymentHash=${add.paymentHash} origin=$origin: htlc timed out") + relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, HtlcTimedout(d.channelId), origin, None, None)) + case None => + // same as for fulfilling the htlc (no big deal) + log.info(s"cannot fail timedout htlc #${add.id} paymentHash=${add.paymentHash} (origin not found)") + } } // we also need to fail outgoing htlcs that we know will never reach the blockchain val overridenHtlcs = Closing.overriddenHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) overridenHtlcs.foreach { add => - val origin = d.commitments.originChannels(add.id) - log.warning(s"failing htlc #${add.id} paymentHash=${add.paymentHash} origin=$origin: overriden by local commit") - relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, HtlcOverridenByLocalCommit(d.channelId), origin, None, None)) + d.commitments.originChannels.get(add.id) match { + case Some(origin) => + log.info(s"failing htlc #${add.id} paymentHash=${add.paymentHash} origin=$origin: overriden by local commit") + relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, HtlcOverridenByLocalCommit(d.channelId), origin, None, None)) + case None => + // same as for fulfilling the htlc (no big deal) + log.info(s"cannot fail overriden htlc #${add.id} paymentHash=${add.paymentHash} (origin not found)") + } } // then let's see if any of the possible close scenarii can be considered done val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed From f3d11cf098a9dd465681f98e167b0175723fdfe4 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Thu, 20 Sep 2018 16:17:15 +0200 Subject: [PATCH 04/31] Fix handling of born again channels (#717) * Fix handling of born again channels When we receive a recent update for a channel that we had marked as stale we must send a query to the underlying transport, not the origin of the update (which would send the query back to the router) --- .../scala/fr/acinq/eclair/router/Router.scala | 24 +++++++------ .../fr/acinq/eclair/router/RouterSpec.scala | 34 +++++++++++++++++-- 2 files changed, 46 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 b99ab813d..7dfbc028a 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 @@ -386,8 +386,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct val ignoredUpdates = getIgnoredChannelDesc(d.updates ++ d.privateUpdates ++ assistedUpdates, ignoreNodes) ++ ignoreChannels ++ d.excludedChannels 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(",")) findRoute(d.graph, start, end, withEdges = assistedUpdates, withoutEdges = ignoredUpdates) - .map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels)) - .recover { case t => sender ! Status.Failure(t) } + .map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels)) + .recover { case t => sender ! Status.Failure(t) } stay case Event(SendChannelQuery(remoteNodeId, remote), d) => @@ -417,10 +417,10 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct log.debug("received channel update from {}", sender) stay using handle(u, sender, d) - case Event(PeerRoutingMessage(_, remoteNodeId, u: ChannelUpdate), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, u: ChannelUpdate), d) => sender ! TransportHandler.ReadAck(u) log.debug("received channel update for shortChannelId={}", u.shortChannelId) - stay using handle(u, sender, d, remoteNodeId_opt = Some(remoteNodeId)) + stay using handle(u, sender, d, remoteNodeId_opt = Some(remoteNodeId), transport_opt = Some(transport)) case Event(PeerRoutingMessage(_, _, c: ChannelAnnouncement), d) => log.debug("received channel announcement for shortChannelId={} nodeId1={} nodeId2={}", c.shortChannelId, c.nodeId1, c.nodeId2) @@ -574,7 +574,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct d } - def handle(u: ChannelUpdate, origin: ActorRef, d: Data, remoteNodeId_opt: Option[PublicKey] = None): Data = + def handle(u: ChannelUpdate, origin: ActorRef, d: Data, remoteNodeId_opt: Option[PublicKey] = None, transport_opt: Option[ActorRef] = None): Data = 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 @@ -657,19 +657,23 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // about that channel. We can ignore this update since we will receive it again log.info(s"channel shortChannelId=${u.shortChannelId} is back from the dead! requesting announcements about this channel") db.removeFromPruned(u.shortChannelId) - remoteNodeId_opt match { - case Some(remoteNodeId) => + + // transport_opt will contain a valid transport only when we're handling an update that we received from a peer, not + // when we're sending updates to ourselves + (transport_opt, remoteNodeId_opt) match { + case (Some(transport), Some(remoteNodeId)) => 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))) case None => // we send the query right away - origin ! QueryShortChannelIds(u.chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(Seq(u.shortChannelId), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + transport ! QueryShortChannelIds(u.chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(Seq(u.shortChannelId), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) d.copy(sync = d.sync + (remoteNodeId -> Sync(missing = SortedSet(u.shortChannelId), totalMissingCount = 1))) } - case None => - // 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) + 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. // 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 } 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 1d22d2b45..048c7eaa1 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 @@ -27,11 +27,13 @@ import fr.acinq.eclair.io.Peer.{InvalidSignature, 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, QueryShortChannelIds} +import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner +import scala.collection.SortedSet +import scala.compat.Platform import scala.concurrent.duration._ /** @@ -232,4 +234,32 @@ class RouterSpec extends BaseRouterSpec { assert(state.nodes.size == 6) assert(state.updates.size == 8) } + + test("ask for channels that we marked as stale for which we receive a new update") { case (router, watcher) => + val blockHeight = Globals.blockCount.get().toInt - 2020 + val channelId = ShortChannelId(blockHeight, 5, 0) + val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) + val timestamp = Platform.currentTime / 1000 - 1209600 - 1 + val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, timestamp = timestamp) + val probe = TestProbe() + probe.ignoreMsg { case _: TransportHandler.ReadAck => true } + probe.send(router, PeerRoutingMessage(null, remoteNodeId, announcement)) + watcher.expectMsgType[ValidateRequest] + probe.send(router, PeerRoutingMessage(null, remoteNodeId, update)) + watcher.send(router, ValidateResult(announcement, Some(Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(1000000), write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0)), true, None)) + + probe.send(router, TickPruneStaleChannels) + val sender = TestProbe() + sender.send(router, GetRoutingState) + val state = sender.expectMsgType[RoutingState] + + + val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, timestamp = Platform.currentTime / 1000) + + // we want to make sure that transport receives the query + val transport = TestProbe() + probe.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, update1)) + val query = transport.expectMsgType[QueryShortChannelIds] + assert(ChannelRangeQueries.decodeShortChannelIds(query.data)._2 == SortedSet(channelId)) + } } From 8d2ec1855747b53060f43fb2c9809cf607dcfb1e Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 20 Sep 2018 16:18:17 +0200 Subject: [PATCH 05/31] Replace `update_fee` in commitments (#709) This is a simple optimisation, we don't have to keep all `update_fee`, just the last one. cf BOLT 2: > An update_fee message is sent by the node which is paying the Bitcoin fee. Like any update, it's first committed to the receiver's commitment transaction and then (once acknowledged) committed to the sender's. Unlike an HTLC, update_fee is never closed but simply replaced. --- .../fr/acinq/eclair/channel/Commitments.scala | 6 +++-- .../channel/states/e/NormalStateSpec.scala | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) 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 fcba08952..9aa38e090 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 @@ -290,7 +290,8 @@ object Commitments { } // let's compute the current commitment *as seen by them* with this change taken into account val fee = UpdateFee(commitments.channelId, cmd.feeratePerKw) - val commitments1 = addLocalProposal(commitments, fee) + // update_fee replace each other, so we can remove previous ones + val commitments1 = commitments.copy(localChanges = commitments.localChanges.copy(proposed = commitments.localChanges.proposed.filterNot(_.isInstanceOf[UpdateFee]) :+ fee)) val reduced = CommitmentSpec.reduce(commitments1.remoteCommit.spec, commitments1.remoteChanges.acked, commitments1.localChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee @@ -324,7 +325,8 @@ object Commitments { // (it also means that we need to check the fee of the initial commitment tx somewhere) // let's compute the current commitment *as seen by us* including this change - val commitments1 = addRemoteProposal(commitments, fee) + // update_fee replace each other, so we can remove previous ones + val commitments1 = commitments.copy(remoteChanges = commitments.remoteChanges.copy(proposed = commitments.remoteChanges.proposed.filterNot(_.isInstanceOf[UpdateFee]) :+ fee)) 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 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 6f6a8e164..e662fcf61 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 @@ -1211,6 +1211,22 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } + test("recv CMD_UPDATE_FEE (two in a row)") { case (alice, _, alice2bob, _, _, _, _) => + within(30 seconds) { + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CMD_UPDATE_FEE(20000)) + sender.expectMsg("ok") + val fee1 = alice2bob.expectMsgType[UpdateFee] + sender.send(alice, CMD_UPDATE_FEE(30000)) + sender.expectMsg("ok") + val fee2 = alice2bob.expectMsgType[UpdateFee] + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee2)))) + } + } + test("recv CMD_UPDATE_FEE (when fundee)") { case (_, bob, _, _, _, _, _) => within(30 seconds) { val sender = TestProbe() @@ -1222,6 +1238,17 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } test("recv UpdateFee") { case (_, bob, _, _, _, _, _) => + within(30 seconds) { + val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] + val fee1 = UpdateFee("00" * 32, 12000) + bob ! fee1 + val fee2 = UpdateFee("00" * 32, 14000) + bob ! fee2 + awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee2), remoteNextHtlcId = 0))) + } + } + + test("recv UpdateFee (two in a row)") { case (_, bob, _, _, _, _, _) => within(30 seconds) { val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] val fee = UpdateFee("00" * 32, 12000) From 6f2a74e0309ea8a2d874755b2a62cb9c9422f958 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Thu, 20 Sep 2018 16:37:57 +0200 Subject: [PATCH 06/31] Tests: use bitcoind 0.16.3 (#715) Bitcoind 0.16.0 is no longer available --- eclair-core/pom.xml | 18 +++++++++--------- .../blockchain/bitcoind/BitcoindService.scala | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 123f944c1..a563e880b 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -79,10 +79,10 @@ true - https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-x86_64-linux-gnu.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-x86_64-linux-gnu.tar.gz - 1375c9f908b0327d9772d4bff0d9f03f - d0b05b51e1d572c44ef5b2cabbfcb662679cf7cb + c371e383f024c6c45fb255d528a6beec + e6d8ab1f7661a6654fd81e236b9b5fd35a3d4dcb @@ -93,10 +93,10 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-osx64.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-osx64.tar.gz - fc10d5cb12a4c3905d97df33f249eb1c - 3b1ab75439ca7a9b547827ec3e59c7e61c1f6fcd + bacd87d0c3f65a5acd666e33d094a59e + 62cc5bd9ced610bb9e8d4a854396bfe2139e3d0f @@ -107,9 +107,9 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-win64.zip - 5b9034754752b7e1b3117eaa5434792e - 90d72e25782a4b454f5f507a26a3cf0f53baecef + https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-win64.zip + bbde9b1206956d19298034319e9f405e + 85e3dc8a9c6f93b1b20cb79fa5850b5ce81da221 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index c1222f83e..67a171abb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -41,7 +41,7 @@ trait BitcoindService extends Logging { val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}" logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.0/bin/bitcoind") + val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.3/bin/bitcoind") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") var bitcoind: Process = null From 8160e793e7905d0706d4d28500628ac30dcdeb1e Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 20 Sep 2018 17:22:38 +0200 Subject: [PATCH 07/31] Make `publishTransaction` idempotent (#711) Bitcoin core returns an error `missing inputs (code: -25)` if the tx that we want to publish has already been published and its output have been spent. When we receive this error, we try to get the tx, in order to know if it is in the blockchain, or if its inputs were spent by another tx. Note: If the outputs of the tx were still unspent, bitcoin core would return "transaction already in block chain (code: -27)" and this is already handled. --- .../blockchain/bitcoind/ZmqWatcher.scala | 1 - .../bitcoind/rpc/ExtendedBitcoinClient.scala | 26 +++- .../bitcoind/ExtendedBitcoinClientSpec.scala | 118 ++++++++++++++++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index aa08262f2..71cb6c1d8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -190,7 +190,6 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = import scala.concurrent.duration._ after(3 seconds, context.system.scheduler)(Future.successful({})).map(x => publish(tx, isRetry = true)) - case t: Throwable if t.getMessage.contains("(code: -27)") => () // "transaction already in block chain (code: -27)" ignore error case t: Throwable => log.error(s"cannot publish tx: reason=${t.getMessage} txid=${tx.txid} tx=$tx") } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala index 2b023e718..b2dd293d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala @@ -106,13 +106,27 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) { future } - def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = - rpcClient.invoke("sendrawtransaction", hex) collect { - case JString(txid) => txid - } - + /** + * Publish a transaction on the bitcoin network. + * + * Note that this method is idempotent, meaning that if the tx was already published a long time ago, then this is + * considered a success even if bitcoin core rejects this new attempt. + * + * @param tx + * @param ec + * @return + */ def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = - publishTransaction(tx.toString()) + rpcClient.invoke("sendrawtransaction", tx.toString()) collect { + case JString(txid) => txid + } recoverWith { + case JsonRPCError(Error(-27, _)) => + // "transaction already in block chain (code: -27)" ignore error + Future.successful(tx.txid.toString()) + case e@JsonRPCError(Error(-25, _)) => + // "missing inputs (code: -25)" it may be that the tx has already been published and its output spent + getRawTransaction(tx.txid.toString()).map { case _ => tx.txid.toString() }.recoverWith { case _ => Future.failed[String](e) } + } /** * We need this to compute absolute timeouts expressed in number of blocks (where getBlockCount would be equivalent diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala new file mode 100644 index 000000000..1b61eb11e --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -0,0 +1,118 @@ +/* + * Copyright 2018 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.blockchain.bitcoind + +import akka.actor.ActorSystem +import akka.actor.Status.Failure +import akka.pattern.pipe +import akka.testkit.{TestKit, TestProbe} +import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.Transaction +import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient} +import grizzled.slf4j.Logging +import org.json4s.JsonAST._ +import org.json4s.{DefaultFormats, JString} +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} + +import scala.collection.JavaConversions._ +import scala.concurrent.ExecutionContext.Implicits.global + +@RunWith(classOf[JUnitRunner]) +class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { + + val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) + val config = ConfigFactory.load(commonConfig).getConfig("eclair") + + implicit val formats = DefaultFormats + + override def beforeAll(): Unit = { + startBitcoind() + } + + override def afterAll(): Unit = { + stopBitcoind() + } + + test("wait bitcoind ready") { + waitForBitcoindReady() + } + + test("send transaction idempotent") { + val bitcoinClient = new BasicBitcoinJsonRPCClient( + user = config.getString("bitcoind.rpcuser"), + password = config.getString("bitcoind.rpcpassword"), + host = config.getString("bitcoind.host"), + port = config.getInt("bitcoind.rpcport")) + + val sender = TestProbe() + bitcoinClient.invoke("getnewaddress").pipeTo(sender.ref) + val JString(address) = sender.expectMsgType[JString] + bitcoinClient.invoke("createrawtransaction", Array.empty, Map(address -> 6)).pipeTo(sender.ref) + val JString(noinputTx) = sender.expectMsgType[JString] + bitcoinClient.invoke("fundrawtransaction", noinputTx).pipeTo(sender.ref) + val json = sender.expectMsgType[JValue] + val JString(unsignedtx) = json \ "hex" + val JInt(changePos) = json \ "changepos" + bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" + val tx = Transaction.read(signedTx) + val txid = tx.txid.toString() + + // test starts here + val client = new ExtendedBitcoinClient(bitcoinClient) + // we publish it a first time + client.publishTransaction(tx).pipeTo(sender.ref) + sender.expectMsg(txid) + // we publish the tx a second time to test idempotence + client.publishTransaction(tx).pipeTo(sender.ref) + sender.expectMsg(txid) + // let's confirm the tx + bitcoinClient.invoke("generate", 1).pipeTo(sender.ref) + sender.expectMsgType[JValue] + // and publish the tx a third time to test idempotence + client.publishTransaction(tx).pipeTo(sender.ref) + sender.expectMsg(txid) + + // now let's spent the output of the tx + val spendingTx = { + val pos = if (changePos == 0) 1 else 0 + bitcoinClient.invoke("createrawtransaction", Array(Map("txid" -> txid, "vout" -> pos)), Map(address -> 5.99999)).pipeTo(sender.ref) + val JString(unsignedtx) = sender.expectMsgType[JValue] + bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" + signedTx + } + bitcoinClient.invoke("sendrawtransaction", spendingTx).pipeTo(sender.ref) + val JString(spendingTxid) = sender.expectMsgType[JValue] + + // and publish the tx a fourth time to test idempotence + client.publishTransaction(tx).pipeTo(sender.ref) + sender.expectMsg(txid) + // let's confirm the tx + bitcoinClient.invoke("generate", 1).pipeTo(sender.ref) + sender.expectMsgType[JValue] + // and publish the tx a fifth time to test idempotence + client.publishTransaction(tx).pipeTo(sender.ref) + sender.expectMsg(txid) + + // this one should be rejected + client.publishTransaction(Transaction.read("02000000000101b9e2a3f518fd74e696d258fed3c78c43f84504e76c99212e01cf225083619acf00000000000d0199800136b34b00000000001600145464ce1e5967773922506e285780339d72423244040047304402206795df1fd93c285d9028c384aacf28b43679f1c3f40215fd7bd1abbfb816ee5a022047a25b8c128e692d4717b6dd7b805aa24ecbbd20cfd664ab37a5096577d4a15d014730440220770f44121ed0e71ec4b482dded976f2febd7500dfd084108e07f3ce1e85ec7f5022025b32dc0d551c47136ce41bfb80f5a10de95c0babb22a3ae2d38e6688b32fcb20147522102c2662ab3e4fa18a141d3be3317c6ee134aff10e6cd0a91282a25bf75c0481ebc2102e952dd98d79aa796289fa438e4fdeb06ed8589ff2a0f032b0cfcb4d7b564bc3252aea58d1120")).pipeTo(sender.ref) + sender.expectMsgType[Failure] + } +} \ No newline at end of file From 83b00e37f40e6457771b6e6c919776a92ca2b75c Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 24 Sep 2018 11:49:05 +0200 Subject: [PATCH 08/31] Improved eclair-cli (#718) This fixes #695, and also adds the channel point in the default channel output. ```bash $ ./eclair-cli channel 00fd4d56d94af93765561bb6cb081f519b9627d3f455eba3215a7846a1af0e46 { "nodeId": "0232e20e7b68b9b673fb25f48322b151a93186bffe4550045040673797ceca43cf", "shortChannelId": "845230006070001", "channelId": "00fd4d56d94af93765561bb6cb081f519b9627d3f455eba3215a7846a1af0e46", "state": "NORMAL", "balanceSat": 9858759, "capacitySat": 10000000, "channelPoint": "470eafa146785a21a3eb55f4d327969b511f08cbb61b566537f94ad9564dfd00:1" } ``` --- eclair-core/eclair-cli | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 3d28660e0..013346957 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -80,17 +80,15 @@ case ${METHOD}_${#} in "receive_2") call ${METHOD} "'$(printf '[%s,"%s"]' "${1}" "${2}")'" ;; # ${1} is numeric (amount to receive) "receive_3") call ${METHOD} "'$(printf '[%s,"%s",%s]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount to receive) as is ${2} for expiry in seconds - "channel_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor), capacitySat: .data.commitments.commitInput.amountSatoshis } end" ;; + "channel_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | { 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 } end" ;; - "channels_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor), capacitySat: .data.commitments.commitInput.amountSatoshis } ) end" ;; - - "channels_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount } ) end" ;; + "channels_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | map( { 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 } ) end" ;; "send_3") call ${METHOD} "'$(printf '[%s,"%s","%s"]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount of the payment) "send_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (amount overriding the payment request) "audit_2") call ${METHOD} "'$(printf '[%s,%s]' "${1}" "${2}")'" ;; # ${1} and ${2} are numeric (unix timestamps) - + "networkfees_2") call ${METHOD} "'$(printf '[%s,%s]' "${1}" "${2}")'" ;; # ${1} and ${2} are numeric (unix timestamps) *) # Default case. From 7a4f175f2fb2764307994c7ad40ffb965e60b47e Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 24 Sep 2018 13:59:01 +0200 Subject: [PATCH 09/31] Handle update relay fee in OFFLINE state (#719) Previously it was only possible to update relay fee in NORMAL state, which is not very convenient because most of the time there are always some channels in OFFLINE state. This works like the NORMAL case, except that the new `channel_update` won't be broadcast immediately. It will be sent out next time the channel goes back to NORMAL, in the same `channel_update` that sets the `enable` flag to true. Also added a default handler that properly rejects the CMD_UPDATE_RELAY_FEE command in all other states. --- .../fr/acinq/eclair/channel/Channel.scala | 12 +++++- .../eclair/channel/ChannelExceptions.scala | 2 +- .../c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../channel/states/e/OfflineStateSpec.scala | 41 +++++++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index dd2886b8c..2d28e64d7 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 @@ -1296,6 +1296,12 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway handleLocalError(HtlcTimedout(d.channelId), d, Some(c)) + case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) => + log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, feeBaseMsat, d.channelUpdate.feeProportionalMillionths, feeProportionalMillionths) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, enable = true) + // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state + stay using store(d.copy(channelUpdate = channelUpdate)) replying "ok" + // just ignore this, we will put a new watch when we reconnect, and we'll be notified again case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK | BITCOIN_FUNDING_DEEPLYBURIED, _, _), _) => stay @@ -1469,14 +1475,16 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case _ => handleCommandError(AddHtlcFailed(d.channelId, c.paymentHash, error, origin(c), None, Some(c)), c) // we don't provide a channel_update: this will be a permanent channel failure } - case Event(c: CMD_CLOSE, d) => handleCommandError(CannotCloseInThisState(Helpers.getChannelId(d), stateName), c) + case Event(c: CMD_CLOSE, d) => handleCommandError(CommandUnavailableInThisState(Helpers.getChannelId(d), "close", stateName), c) case Event(c@CMD_FORCECLOSE, d) => d match { case data: HasCommitments => handleLocalError(ForcedLocalCommit(data.channelId), data, Some(c)) replying "ok" - case _ => handleCommandError(CannotCloseInThisState(Helpers.getChannelId(d), stateName), c) + case _ => handleCommandError(CommandUnavailableInThisState(Helpers.getChannelId(d), "forceclose", stateName), c) } + case Event(c: CMD_UPDATE_RELAY_FEE, d) => handleCommandError(CommandUnavailableInThisState(Helpers.getChannelId(d), "updaterelayfee", stateName), c) + // we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream case Event(CurrentBlockCount(_), _) => stay 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 809a5279d..e8b09888b 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 @@ -43,7 +43,6 @@ case class ChannelReserveNotMet (override val channelId: BinaryDa case class ChannelFundingError (override val channelId: BinaryData) extends ChannelException(channelId, "channel funding error") case class NoMoreHtlcsClosingInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress") case class ClosingAlreadyInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "closing already in progress") -case class CannotCloseInThisState (override val channelId: BinaryData, state: State) extends ChannelException(channelId, s"cannot close in state=$state") case class CannotCloseWithUnsignedOutgoingHtlcs(override val channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs") case class ChannelUnavailable (override val channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)") case class InvalidFinalScript (override val channelId: BinaryData) extends ChannelException(channelId, "invalid final script") @@ -82,4 +81,5 @@ case class RevocationSyncError (override val channelId: BinaryDa case class InvalidFailureCode (override val channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set") case class PleasePublishYourCommitment (override val channelId: BinaryData) extends ChannelException(channelId, "please publish your local commitment") case class AddHtlcFailed (override val channelId: BinaryData, paymentHash: BinaryData, t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate], originalCommand: Option[CMD_ADD_HTLC]) extends ChannelException(channelId, s"cannot add htlc with origin=$origin reason=${t.getMessage}") +case class CommandUnavailableInThisState (override val channelId: BinaryData, command: String, state: State) extends ChannelException(channelId, s"cannot execute command=$command in state=$state") // @formatter:on \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 48c31cfb0..b40638e35 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -133,7 +133,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH within(30 seconds) { val sender = TestProbe() sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(CannotCloseInThisState(channelId(alice), WAIT_FOR_FUNDING_CONFIRMED))) + sender.expectMsg(Failure(CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_CONFIRMED))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 605e0da34..d53889e48 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -116,7 +116,7 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp within(30 seconds) { val sender = TestProbe() sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(CannotCloseInThisState(channelId(alice), WAIT_FOR_FUNDING_LOCKED))) + sender.expectMsg(Failure(CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_LOCKED))) } } 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 85dc898d7..cfc095011 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 @@ -23,6 +23,7 @@ import fr.acinq.eclair.blockchain.{PublishAsap, WatchEventSpent} import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.junit.runner.RunWith @@ -318,4 +319,44 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(new String(error.data) === InvalidRevokedCommitProof(channelId(alice), 0, 42, ba_reestablish_forged.yourLastPerCommitmentSecret.get).getMessage) } + test("change relay fee while offline") { case (alice, bob, alice2bob, bob2alice, _, _, relayer) => + + val sender = TestProbe() + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // alice and bob announce that their channel is OFFLINE + assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.flags) == false) + assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.flags) == false) + + // we make alice update here relay fee + sender.send(alice, CMD_UPDATE_RELAY_FEE(4200, 123456)) + sender.expectMsg("ok") + + // alice doesn't broadcast the new channel_update yet + relayer.expectNoMsg(300 millis) + + // then we reconnect them + sender.send(alice, INPUT_RECONNECTED(alice2bob.ref)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref)) + + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + bob2alice.expectMsgType[ChannelReestablish] + // note that we don't forward the channel_reestablish so that only alice reaches NORMAL state, it facilitates the test below + bob2alice.forward(alice) + + // then alice reaches NORMAL state, and during the transition she broadcasts the channel_update + val channelUpdate = relayer.expectMsgType[LocalChannelUpdate](10 seconds).channelUpdate + assert(channelUpdate.feeBaseMsat === 4200) + assert(channelUpdate.feeProportionalMillionths === 123456) + + // no more messages + relayer.expectNoMsg(300 millis) + } + } From f38748c0b6132e024d1c098ea36d35fe9d252550 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 24 Sep 2018 19:41:27 +0200 Subject: [PATCH 10/31] Fixed regression caused by 7a4f175 (#722) When updating relay fee in state OFFLINE, the new channel_update must have the disabled flag on. This caused tests to be flaky, added necessary checks to always make them fail in case that kind of regression happens again. --- .../fr/acinq/eclair/channel/Channel.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 24 +++++++++++++++++-- .../channel/states/e/OfflineStateSpec.scala | 1 + 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 2d28e64d7..64d649897 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 @@ -1298,7 +1298,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) => log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, feeBaseMsat, d.channelUpdate.feeProportionalMillionths, feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, enable = true) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, enable = false) // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state stay using store(d.copy(channelUpdate = channelUpdate)) replying "ok" 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 e662fcf61..75e006fea 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 @@ -2024,7 +2024,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } - test("recv TickRefreshChannelUpdate", Tag("channels_public")) { case (alice, bob, alice2bob, bob2alice, _, _, relayer) => + test("recv TickRefreshChannelUpdate", Tag("channels_public")) { case (alice, bob, _, bob2alice, _, _, relayer) => within(30 seconds) { val sender = TestProbe() sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) @@ -2034,11 +2034,31 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val update1 = relayer.expectMsgType[LocalChannelUpdate] // actual test starts here - Thread.sleep(1000) + Thread.sleep(1100) sender.send(alice, TickRefreshChannelUpdate) val update2 = relayer.expectMsgType[LocalChannelUpdate] assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) } } + test("recv INPUT_DISCONNECTED", Tag("channels_public")) { case (alice, bob, _, bob2alice, _, _, relayer) => + within(30 seconds) { + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + val update1 = relayer.expectMsgType[LocalChannelUpdate] + assert(Announcements.isEnabled(update1.channelUpdate.flags) == true) + + // actual test starts here + Thread.sleep(1100) + sender.send(alice, INPUT_DISCONNECTED) + val update2 = relayer.expectMsgType[LocalChannelUpdate] + assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) + assert(Announcements.isEnabled(update2.channelUpdate.flags) == false) + awaitCond(alice.stateName == OFFLINE) + } + } + } 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 cfc095011..5434b265e 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 @@ -354,6 +354,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val channelUpdate = relayer.expectMsgType[LocalChannelUpdate](10 seconds).channelUpdate assert(channelUpdate.feeBaseMsat === 4200) assert(channelUpdate.feeProportionalMillionths === 123456) + assert(Announcements.isEnabled(channelUpdate.flags) == true) // no more messages relayer.expectNoMsg(300 millis) From b07bb2a39e0aedff58eba8df656e427dffb260c1 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Mon, 24 Sep 2018 20:00:36 +0200 Subject: [PATCH 11/31] Logging: use a rolling file appender (#721) * Logging: use a rolling file appender Use one file per day, keep 90 days of logs with a total maximum size capped at 5 Gb * Router: log routing broadcast in debug level only --- .../main/scala/fr/acinq/eclair/router/Router.scala | 2 +- eclair-node/src/main/resources/logback.xml | 12 +++++++++--- 2 files changed, 10 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 7dfbc028a..19c73b8b2 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 @@ -309,7 +309,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct if (d.rebroadcast.channels.isEmpty && d.rebroadcast.updates.isEmpty && d.rebroadcast.nodes.isEmpty) { stay } else { - log.info("broadcasting routing messages") + log.debug("broadcasting routing messages") log.debug("staggered broadcast details: channels={} updates={} nodes={}", d.rebroadcast.channels.size, d.rebroadcast.updates.size, d.rebroadcast.nodes.size) context.actorSelection(context.system / "*" / "switchboard") ! d.rebroadcast stay using d.copy(rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty)) diff --git a/eclair-node/src/main/resources/logback.xml b/eclair-node/src/main/resources/logback.xml index 9d669c208..59f3871af 100644 --- a/eclair-node/src/main/resources/logback.xml +++ b/eclair-node/src/main/resources/logback.xml @@ -25,9 +25,15 @@ - + ${eclair.datadir:-${user.home}/.eclair}/eclair.log - true + + + ${eclair.datadir:-${user.home}/.eclair}/eclair.%d{yyyy-MM-dd}.log + + 90 + 5GB + %d %-5level %logger{24} %X{nodeId}%X{channelId} - %msg%ex{24}%n @@ -47,7 +53,7 @@ - + \ No newline at end of file From 3fc5da0a7b612b01fdcd2bc5f009996f69ab5e7b Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 26 Sep 2018 14:04:38 +0200 Subject: [PATCH 12/31] set version to 0.2-beta6 --- eclair-core/pom.xml | 2 +- eclair-node-gui/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index a563e880b..36660eb9e 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta6 eclair-core_2.11 diff --git a/eclair-node-gui/pom.xml b/eclair-node-gui/pom.xml index 293177cee..953c4917f 100644 --- a/eclair-node-gui/pom.xml +++ b/eclair-node-gui/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta6 eclair-node-gui_2.11 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 36c687f81..f4f9e1662 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta6 eclair-node_2.11 diff --git a/pom.xml b/pom.xml index 815baac25..03e941583 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta6 pom From 997aceea82942769649bf03e95c5ea549a7beb0c Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 26 Sep 2018 14:05:57 +0200 Subject: [PATCH 13/31] set version back to 0.2-SNAPSHOT --- eclair-core/pom.xml | 2 +- eclair-node-gui/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 36660eb9e..a563e880b 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta6 + 0.2-SNAPSHOT eclair-core_2.11 diff --git a/eclair-node-gui/pom.xml b/eclair-node-gui/pom.xml index 953c4917f..293177cee 100644 --- a/eclair-node-gui/pom.xml +++ b/eclair-node-gui/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta6 + 0.2-SNAPSHOT eclair-node-gui_2.11 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index f4f9e1662..36c687f81 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta6 + 0.2-SNAPSHOT eclair-node_2.11 diff --git a/pom.xml b/pom.xml index 03e941583..815baac25 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta6 + 0.2-SNAPSHOT pom From d490480b7ea7a147161d3325cc10e900a61897ca Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Thu, 11 Oct 2018 18:54:12 +0200 Subject: [PATCH 14/31] Simplify bitcoind version check (#731) Bitcoind returns version as MMmmrr (major, minor, revision), use an int representation and compare it to our minimum version target. --- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 2a31f5769..f5a8be973 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -101,7 +101,7 @@ class Setup(datadir: File, blocks = (json \ "blocks").extract[Long] headers = (json \ "headers").extract[Long] chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_)).map(x => BinaryData(x.reverse)) - bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String]) + bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[Int]) unspentAddresses <- bitcoinClient.invoke("listunspent").collect { case JArray(values) => values .filter(value => (value \ "spendable").extract[Boolean]) @@ -110,7 +110,7 @@ class Setup(datadir: File, } yield (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) // blocking sanity checks val (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") - assert(bitcoinVersion.startsWith("16"), "Eclair requires Bitcoin Core 0.16.0 or higher") + assert(bitcoinVersion >= 160300, "Eclair requires Bitcoin Core 0.16.3 or higher") assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") if (chainHash != Block.RegtestGenesisBlock.hash) { assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Make sure that all your UTXOS are segwit UTXOS and not p2pkh (check out our README for more details)") From 892770b1edb0f68dccff7617b71a66546c5ef14d Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 11 Oct 2018 19:01:41 +0200 Subject: [PATCH 15/31] Update scalatest and remove junit runner (#728) * updated to scalatest 3.0.5 * use scalatest runner instead of junit Output is far more readable, and makes console (incl. travis) reports actually usable. Turned off test logs as error reporting is enough to figure out what happens. The only downside is that we can't use junit's categories to group tests, like we did for docker related tests. We could use nested suites, but that seems to be overkill so I just removed the categories. Users will only have the possibility to either skip/run all tests. * update scala-maven-plugin to 3.4.2 NB: This requires maven 3.5.4, which means that we currently need to manually install maven on travis. Also updated Docker java version to 8u181 (8u171 for compiling). --- .travis.yml | 5 + BUILD.md | 6 +- Dockerfile | 11 +- .../src/test/resources/logback-test.xml | 4 +- .../scala/fr/acinq/eclair/CoinUtilsSpec.scala | 4 +- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 4 +- .../scala/fr/acinq/eclair/PackageSpec.scala | 4 +- .../fr/acinq/eclair/ShortChannelIdSpec.scala | 4 +- .../scala/fr/acinq/eclair/TestConstants.scala | 1 - .../scala/fr/acinq/eclair/UInt64Spec.scala | 4 +- .../eclair/api/JsonSerializersSpec.scala | 4 +- .../acinq/eclair/blockchain/WatcherSpec.scala | 4 +- .../bitcoind/BitcoinCoreWalletSpec.scala | 4 +- .../bitcoind/ExtendedBitcoinClientSpec.scala | 4 +- .../electrum/ElectrumClientPoolSpec.scala | 4 +- .../electrum/ElectrumClientSpec.scala | 4 +- .../electrum/ElectrumWalletBasicSpec.scala | 4 +- .../ElectrumWalletSimulatedClientSpec.scala | 4 +- .../electrum/ElectrumWalletSpec.scala | 8 +- .../electrum/ElectrumWatcherSpec.scala | 6 +- .../electrum/ElectrumxService.scala | 2 +- .../fee/BitcoinCoreFeeProviderSpec.scala | 6 +- .../blockchain/fee/BitgoFeeProviderSpec.scala | 4 +- .../fee/EarnDotComFeeProviderSpec.scala | 4 +- .../fee/FallbackFeeProviderSpec.scala | 4 +- .../fee/SmoothFeeProviderSpec.scala | 8 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 114 +- .../acinq/eclair/channel/ThroughputSpec.scala | 4 +- .../a/WaitForAcceptChannelStateSpec.scala | 174 +- .../a/WaitForOpenChannelStateSpec.scala | 251 +- ...itForFundingCreatedInternalStateSpec.scala | 33 +- .../b/WaitForFundingCreatedStateSpec.scala | 72 +- .../b/WaitForFundingSignedStateSpec.scala | 61 +- .../c/WaitForFundingConfirmedStateSpec.scala | 144 +- .../c/WaitForFundingLockedStateSpec.scala | 108 +- .../channel/states/e/NormalStateSpec.scala | 3791 ++++++++--------- .../channel/states/e/OfflineStateSpec.scala | 34 +- .../channel/states/f/ShutdownStateSpec.scala | 1156 +++-- .../states/g/NegotiatingStateSpec.scala | 273 +- .../channel/states/h/ClosingStateSpec.scala | 746 ++-- .../acinq/eclair/crypto/BitStreamSpec.scala | 4 +- .../eclair/crypto/ChaCha20Poly1305Spec.scala | 4 +- .../acinq/eclair/crypto/GeneratorsSpec.scala | 4 +- .../eclair/crypto/LocalKeyManagerSpec.scala | 4 +- .../fr/acinq/eclair/crypto/NoiseSpec.scala | 4 +- .../fr/acinq/eclair/crypto/ShaChainSpec.scala | 4 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 5 +- .../eclair/crypto/TransportHandlerSpec.scala | 4 +- .../fr/acinq/eclair/db/ChannelStateSpec.scala | 4 +- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 4 +- .../eclair/db/SqliteChannelsDbSpec.scala | 4 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 6 +- .../eclair/db/SqlitePaymentsDbSpec.scala | 4 +- .../acinq/eclair/db/SqlitePeersDbSpec.scala | 4 +- .../eclair/db/SqlitePendingRelayDbSpec.scala | 4 +- .../eclair/integration/IntegrationSpec.scala | 4 +- .../interop/rustytests/RustyTestsSpec.scala | 40 +- .../fr/acinq/eclair/io/NodeURISpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 22 +- .../eclair/payment/HtlcGenerationSpec.scala | 4 +- .../eclair/payment/PaymentHandlerSpec.scala | 4 +- .../eclair/payment/PaymentLifecycleSpec.scala | 35 +- .../eclair/payment/PaymentRequestSpec.scala | 4 +- .../fr/acinq/eclair/payment/RelayerSpec.scala | 56 +- .../AnnouncementsBatchValidationSpec.scala | 6 +- .../eclair/router/AnnouncementsSpec.scala | 4 +- .../acinq/eclair/router/BaseRouterSpec.scala | 13 +- .../router/ChannelRangeQueriesSpec.scala | 4 +- .../eclair/router/RouteCalculationSpec.scala | 7 +- .../fr/acinq/eclair/router/RouterSpec.scala | 51 +- .../acinq/eclair/router/RoutingSyncSpec.scala | 6 +- .../transactions/ClaimReceivedHtlcSpec.scala | 4 +- .../transactions/ClaimSentHtlcSpec.scala | 4 +- .../transactions/CommitmentSpecSpec.scala | 4 +- .../transactions/TransactionsSpec.scala | 4 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 4 +- .../acinq/eclair/wire/CommandCodecsSpec.scala | 4 +- .../wire/FailureMessageCodecsSpec.scala | 4 +- .../wire/LightningMessageCodecsSpec.scala | 4 +- pom.xml | 45 +- 80 files changed, 3579 insertions(+), 3907 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5214a79ec..50b8f56a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,11 @@ scala: - 2.11.11 env: - export LD_LIBRARY_PATH=/usr/local/lib +before_install: + - wget http://mirror.ibcp.fr/pub/apache/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.zip + - unzip -qq apache-maven-3.5.4-bin.zip + - export M2_HOME=$PWD/apache-maven-3.5.4 + - export PATH=$M2_HOME/bin:$PATH script: - mvn install cache: diff --git a/BUILD.md b/BUILD.md index fb1bbfb0b..699b57e69 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,7 +2,7 @@ ## Requirements - [Java Development Kit](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 1.8u161 or newer -- [Maven](https://maven.apache.org/download.cgi) 3.3.x or newer +- [Maven](https://maven.apache.org/download.cgi) 3.5.4 or newer - [Inno Setup](http://www.jrsoftware.org/isdl.php) 5.5.9 (optional, if you want to generate the windows installer) - [Docker](https://www.docker.com/) 18.03 or newer (optional) if you want to run all tests @@ -14,10 +14,6 @@ $ mvn install #### Other build options -If you don't have Docker you can skip tests that depend on it: -```shell -$ mvn install -DexcludedGroups=fr.acinq.eclair.blockchain.electrum.DockerTest -``` To skip all tests, run: ```shell $ mvn install -DskipTests diff --git a/Dockerfile b/Dockerfile index ed09582d8..00ca879c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:8u151-jdk-alpine as BUILD +FROM openjdk:8u171-jdk-alpine as BUILD # Setup maven, we don't use https://hub.docker.com/_/maven/ as it declare .m2 as volume, we loose all mvn cache # We can alternatively do as proposed by https://github.com/carlossg/docker-maven#packaging-a-local-repository-with-the-image @@ -6,9 +6,9 @@ FROM openjdk:8u151-jdk-alpine as BUILD RUN apk add --no-cache curl tar bash -ARG MAVEN_VERSION=3.3.9 +ARG MAVEN_VERSION=3.5.4 ARG USER_HOME_DIR="/root" -ARG SHA=6e3e9c949ab4695a204f74038717aa7b2689b1be94875899ac1b3fe42800ff82 +ARG SHA=ce50b1c91364cb77efe3776f756a6d92b76d9038b0a0782f7d53acf1e997a14d ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries RUN mkdir -p /usr/share/maven /usr/share/maven/ref \ @@ -31,7 +31,8 @@ COPY eclair-node/pom.xml eclair-node/pom.xml COPY eclair-node-gui/pom.xml eclair-node-gui/pom.xml RUN mkdir -p eclair-core/src/main/scala && touch eclair-core/src/main/scala/empty.scala # Blank build. We only care about eclair-node, and we use install because eclair-node depends on eclair-core -RUN mvn install -pl eclair-node -am clean +RUN mvn install -pl eclair-node -am +RUN mvn clean # Only then do we copy the sources COPY . . @@ -41,7 +42,7 @@ RUN mvn package -pl eclair-node -am -DskipTests -Dgit.commit.id=notag -Dgit.comm # It might be good idea to run the tests here, so that the docker build fail if the code is bugged # We currently use a debian image for runtime because of some jni-related issue with sqlite -FROM openjdk:8u171-jre-slim +FROM openjdk:8u181-jre-slim WORKDIR /app # Eclair only needs the eclair-node-*.jar to run COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.jar . diff --git a/eclair-core/src/test/resources/logback-test.xml b/eclair-core/src/test/resources/logback-test.xml index 57edd28ba..f8c06ca93 100644 --- a/eclair-core/src/test/resources/logback-test.xml +++ b/eclair-core/src/test/resources/logback-test.xml @@ -49,8 +49,8 @@ - + + \ No newline at end of file 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 f0f83e42b..68ce42634 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala @@ -17,11 +17,9 @@ package fr.acinq.eclair import fr.acinq.bitcoin.{Btc, MilliBtc, MilliSatoshi, Satoshi} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class CoinUtilsSpec extends FunSuite { test("Convert string amount to the correct BtcAmount") { 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 7a874d9fb..c629470ff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -20,14 +20,12 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Protocol import fr.acinq.eclair.Features._ -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner /** * Created by PM on 27/01/2017. */ -@RunWith(classOf[JUnitRunner]) + class FeaturesSpec extends FunSuite { test("'initial_routing_sync' feature") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala index dd9f8b6b5..12eafa3ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala @@ -18,16 +18,14 @@ package fr.acinq.eclair import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, BinaryData, Block, Crypto, Script} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.util.Try /** * Created by PM on 27/01/2017. */ -@RunWith(classOf[JUnitRunner]) + class PackageSpec extends FunSuite { test("compute long channel id") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala index ce4a45a71..d6eb0f22c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala @@ -16,12 +16,10 @@ package fr.acinq.eclair -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class ShortChannelIdSpec extends FunSuite { test("handle values from 0 to 0xffffffffffff") { 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 19188c7e8..117ea4a7d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -22,7 +22,6 @@ import java.sql.DriverManager import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{BinaryData, Block, Script} import fr.acinq.eclair.NodeParams.BITCOIND -import fr.acinq.eclair.TestConstants.Alice.sqlite import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala index 596858b49..94cebea18 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala @@ -17,12 +17,10 @@ package fr.acinq.eclair import fr.acinq.bitcoin.BinaryData -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class UInt64Spec extends FunSuite { test("handle values from 0 to 2^63-1") { 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 e80a1d436..9ec9f58ee 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 @@ -22,11 +22,9 @@ import fr.acinq.bitcoin.{BinaryData, OutPoint} import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.NodeAddress import org.json4s.jackson.Serialization -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{FunSuite, Matchers} -@RunWith(classOf[JUnitRunner]) + class JsonSerializersSpec extends FunSuite with Matchers { test("deserialize Map[OutPoint, BinaryData]") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala index 8551ed172..3b6110c89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala @@ -17,14 +17,12 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.Transaction -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner /** * Created by PM on 27/01/2017. */ -@RunWith(classOf[JUnitRunner]) + class WatcherSpec extends FunSuite { test("extract pay2wpkh pubkey script") { 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 d12c4bc17..e0a648478 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 @@ -30,8 +30,6 @@ import fr.acinq.eclair.{addressToPublicKeyScript, randomKey} import grizzled.slf4j.Logging import org.json4s.JsonAST._ import org.json4s.{DefaultFormats, JString} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.collection.JavaConversions._ @@ -40,7 +38,7 @@ import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Random, Try} -@RunWith(classOf[JUnitRunner]) + class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala index 1b61eb11e..d730b2bf1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -26,14 +26,12 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, Exten import grizzled.slf4j.Logging import org.json4s.JsonAST._ import org.json4s.{DefaultFormats, JString} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.collection.JavaConversions._ import scala.concurrent.ExecutionContext.Implicits.global -@RunWith(classOf[JUnitRunner]) + class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala index ca9cd43f9..53034b24c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala @@ -21,14 +21,12 @@ import akka.testkit.{TestKit, TestProbe} import fr.acinq.bitcoin.{BinaryData, Crypto, Transaction} import fr.acinq.eclair.blockchain.electrum.ElectrumClient._ import grizzled.slf4j.Logging -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.concurrent.duration._ import scala.util.Random -@RunWith(classOf[JUnitRunner]) + class ElectrumClientPoolSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with Logging with BeforeAndAfterAll { var router: ActorRef = _ val probe = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala index 006b13d02..6763d77b6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala @@ -22,14 +22,12 @@ import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} import fr.acinq.bitcoin.{BinaryData, Crypto, Transaction} import grizzled.slf4j.Logging -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ -@RunWith(classOf[JUnitRunner]) + class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with Logging with BeforeAndAfterAll { import ElectrumClient._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala index 64b1d07e2..074117a10 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala @@ -21,13 +21,11 @@ import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKe import fr.acinq.bitcoin._ import fr.acinq.eclair.transactions.Transactions import grizzled.slf4j.Logging -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.util.{Failure, Random, Success, Try} -@RunWith(classOf[JUnitRunner]) + class ElectrumWalletBasicSpec extends FunSuite with Logging { import ElectrumWallet._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala index 193fa863b..6cf45c183 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala @@ -23,13 +23,11 @@ import akka.testkit.{TestFSMRef, TestKit, TestProbe} import fr.acinq.bitcoin.{BinaryData, Block, MnemonicCode, Satoshi} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ScriptHashSubscription, ScriptHashSubscriptionResponse} import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._ -import org.junit.runner.RunWith import org.scalatest.FunSuiteLike -import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ -@RunWith(classOf[JUnitRunner]) + class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala index d123ff772..02b286256 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala @@ -27,20 +27,14 @@ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionRes import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse} import grizzled.slf4j.Logging -import org.json4s.JsonAST.{JDecimal, JDouble, JString, JValue} -import org.junit.experimental.categories.Category -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.json4s.JsonAST.{JDecimal, JString, JValue} import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ -trait DockerTest {} -@RunWith(classOf[JUnitRunner]) -@Category(Array(classOf[DockerTest])) class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { import ElectrumWallet._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala index e520cac0d..e9dc3a340 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala @@ -27,15 +27,11 @@ import fr.acinq.eclair.blockchain.{WatchConfirmed, WatchEventConfirmed, WatchEve import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT} import grizzled.slf4j.Logging import org.json4s.JsonAST.{JArray, JString, JValue} -import org.junit.experimental.categories.Category -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.concurrent.duration._ -@RunWith(classOf[JUnitRunner]) -@Category(Array(classOf[DockerTest])) + class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { override def beforeAll(): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala index 01094588e..3df6908f6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.blockchain.electrum import com.spotify.docker.client.{DefaultDockerClient, DockerClient} import com.whisk.docker.impl.spotify.SpotifyDockerFactory import com.whisk.docker.scalatest.DockerTestKit -import com.whisk.docker.{DockerContainer, DockerFactory, LogLineReceiver} +import com.whisk.docker.{DockerContainer, DockerFactory} import org.scalatest.Suite trait ElectrumxService extends DockerTestKit { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 3d4681dcf..493a835e7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -7,13 +7,11 @@ import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin._ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.BasicBitcoinJsonRPCClient import grizzled.slf4j.Logging import org.json4s.DefaultFormats import org.json4s.JsonAST._ import org.json4s.jackson.JsonMethods -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.collection.JavaConversions._ @@ -21,7 +19,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ExecutionContext, Future} import scala.util.Random -@RunWith(classOf[JUnitRunner]) + class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala index 1bf7bb05d..f4368275e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala @@ -19,14 +19,12 @@ package fr.acinq.eclair.blockchain.fee import akka.actor.ActorSystem import akka.util.Timeout import org.json4s.DefaultFormats -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner /** * Created by PM on 27/01/2017. */ -@RunWith(classOf[JUnitRunner]) + class BitgoFeeProviderSpec extends FunSuite { import BitgoFeeProvider._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala index d2fc8a2ad..a0c79750e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala @@ -20,16 +20,14 @@ import akka.actor.ActorSystem import akka.util.Timeout import grizzled.slf4j.Logging import org.json4s.DefaultFormats -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.concurrent.Await /** * Created by PM on 27/01/2017. */ -@RunWith(classOf[JUnitRunner]) + class EarnDotComFeeProviderSpec extends FunSuite with Logging { import EarnDotComFeeProvider._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala index a527307b4..f61c16857 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala @@ -16,15 +16,13 @@ package fr.acinq.eclair.blockchain.fee -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ import scala.concurrent.{Await, Future} import scala.util.Random -@RunWith(classOf[JUnitRunner]) + class FallbackFeeProviderSpec extends FunSuite { import scala.concurrent.ExecutionContext.Implicits.global diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala index 1435011b4..882bd3da8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala @@ -1,14 +1,12 @@ package fr.acinq.eclair.blockchain.fee -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -import scala.concurrent.{Await, Future} -import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} + -@RunWith(classOf[JUnitRunner]) class SmoothFeeProviderSpec extends FunSuite { test("smooth fee rates") { val rates = Array( 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 09451818f..a3619e086 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 @@ -31,9 +31,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire._ import grizzled.slf4j.Logging -import org.junit.runner.RunWith import org.scalatest.Tag -import org.scalatest.junit.JUnitRunner import scala.collection.immutable.Nil import scala.concurrent.duration._ @@ -42,10 +40,10 @@ import scala.util.Random /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Logging { - type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], ActorRef, ActorRef, ActorRef, ActorRef, ActorRef] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], pipe: ActorRef, relayerA: ActorRef, relayerB: ActorRef, paymentHandlerA: ActorRef, paymentHandlerB: ActorRef) override def withFixture(test: OneArgTest) = { val fuzzy = test.tags.contains("fuzzy") @@ -82,7 +80,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) } - test((alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB)) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB))) } class SenderActor(channel: TestFSMRef[State, Data, Channel], paymentHandler: ActorRef, latch: CountDownLatch) extends Actor with ActorLogging { @@ -133,65 +131,65 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi } - test("fuzzy test with only one party sending HTLCs", Tag("fuzzy")) { - case (alice, bob, _, _, _, _, paymentHandlerB) => - val latch = new CountDownLatch(100) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) - awaitCond(latch.getCount == 0, max = 2 minutes) - assert(alice.stateName == NORMAL || alice.stateName == OFFLINE) - assert(bob.stateName == NORMAL || alice.stateName == OFFLINE) + test("fuzzy test with only one party sending HTLCs", Tag("fuzzy")) { f => + import f._ + val latch = new CountDownLatch(100) + system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) + system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) + awaitCond(latch.getCount == 0, max = 2 minutes) + assert(alice.stateName == NORMAL || alice.stateName == OFFLINE) + assert(bob.stateName == NORMAL || alice.stateName == OFFLINE) } - test("fuzzy test with both parties sending HTLCs", Tag("fuzzy")) { - case (alice, bob, _, _, _, paymentHandlerA, paymentHandlerB) => - val latch = new CountDownLatch(100) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) - awaitCond(latch.getCount == 0, max = 2 minutes) - assert(alice.stateName == NORMAL || alice.stateName == OFFLINE) - assert(bob.stateName == NORMAL || alice.stateName == OFFLINE) + test("fuzzy test with both parties sending HTLCs", Tag("fuzzy")) { f => + import f._ + val latch = new CountDownLatch(100) + system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) + system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) + system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) + system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) + awaitCond(latch.getCount == 0, max = 2 minutes) + assert(alice.stateName == NORMAL || alice.stateName == OFFLINE) + assert(bob.stateName == NORMAL || alice.stateName == OFFLINE) } - test("one party sends lots of htlcs send shutdown") { - case (alice, _, _, _, _, _, paymentHandlerB) => - val latch = new CountDownLatch(20) - val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: Nil - awaitCond(latch.getCount == 0, max = 2 minutes) - val sender = TestProbe() - awaitCond({ - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) == "ok" - }, max = 30 seconds) - senders.foreach(_ ! 'stop) - awaitCond(alice.stateName == CLOSING) - awaitCond(alice.stateName == CLOSING) + test("one party sends lots of htlcs send shutdown") { f => + import f._ + val latch = new CountDownLatch(20) + val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: + system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: + system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: Nil + awaitCond(latch.getCount == 0, max = 2 minutes) + val sender = TestProbe() + awaitCond({ + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) == "ok" + }, max = 30 seconds) + senders.foreach(_ ! 'stop) + awaitCond(alice.stateName == CLOSING) + awaitCond(alice.stateName == CLOSING) } - test("both parties send lots of htlcs send shutdown") { - case (alice, bob, _, _, _, paymentHandlerA, paymentHandlerB) => - val latch = new CountDownLatch(30) - val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) :: - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) :: Nil - awaitCond(latch.getCount == 0, max = 2 minutes) - val sender = TestProbe() - awaitCond({ - sender.send(alice, CMD_CLOSE(None)) - val resa = sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) - sender.send(bob, CMD_CLOSE(None)) - val resb = sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) - // we only need that one of them succeeds - resa == "ok" || resb == "ok" - }, max = 30 seconds) - senders.foreach(_ ! 'stop) - awaitCond(alice.stateName == CLOSING) - awaitCond(alice.stateName == CLOSING) + test("both parties send lots of htlcs send shutdown") { f => + import f._ + val latch = new CountDownLatch(30) + val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: + system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: + system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) :: + system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) :: Nil + awaitCond(latch.getCount == 0, max = 2 minutes) + val sender = TestProbe() + awaitCond({ + sender.send(alice, CMD_CLOSE(None)) + val resa = sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) + sender.send(bob, CMD_CLOSE(None)) + val resb = sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) + // we only need that one of them succeeds + resa == "ok" || resb == "ok" + }, max = 30 seconds) + senders.foreach(_ ! 'stop) + awaitCond(alice.stateName == CLOSING) + awaitCond(alice.stateName == CLOSING) } } 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 b5875fff0..83bdeabb3 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 @@ -28,14 +28,12 @@ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.payment.Relayer import fr.acinq.eclair.wire.{Init, UpdateAddHtlc} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ import scala.util.Random -@RunWith(classOf[JUnitRunner]) + class ThroughputSpec extends FunSuite { ignore("throughput") { implicit val system = ActorSystem() 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 51cd6bc85..bf8c011f7 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 @@ -23,21 +23,19 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_INTERNAL, _} import fr.acinq.eclair.wire.{AcceptChannel, Error, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.Tag -import org.scalatest.junit.JUnitRunner +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = if (test.tags.contains("mainnet")) { init(TestConstants.Alice.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), TestConstants.Bob.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash)) } else { @@ -52,112 +50,102 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) - } - test((alice, alice2bob, bob2alice, alice2blockchain)) - } - - test("recv AcceptChannel") { case (alice, _, bob2alice, _) => - within(30 seconds) { - bob2alice.expectMsgType[AcceptChannel] - bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) } } - test("recv AcceptChannel (invalid max accepted htlcs)") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - val accept = bob2alice.expectMsgType[AcceptChannel] - // spec says max = 483 - val invalidMaxAcceptedHtlcs = 484 - alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) - val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage.getBytes("UTF-8"))) - awaitCond(alice.stateName == CLOSED) - } + test("recv AcceptChannel") { f => + import f._ + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) } - test("recv AcceptChannel (invalid dust limit)", Tag("mainnet")) { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - val accept = bob2alice.expectMsgType[AcceptChannel] - // we don't want their dust limit to be below 546 - val lowDustLimitSatoshis = 545 - alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis) - val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUSTLIMIT).getMessage.getBytes("UTF-8"))) - awaitCond(alice.stateName == CLOSED) - } + test("recv AcceptChannel (invalid max accepted htlcs)") { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + // spec says max = 483 + val invalidMaxAcceptedHtlcs = 484 + alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage.getBytes("UTF-8"))) + awaitCond(alice.stateName == CLOSED) } - test("recv AcceptChannel (to_self_delay too high)") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - val accept = bob2alice.expectMsgType[AcceptChannel] - val delayTooHigh = 10000 - alice ! accept.copy(toSelfDelay = delayTooHigh) - val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelayBlocks).getMessage.getBytes("UTF-8"))) - awaitCond(alice.stateName == CLOSED) - } + test("recv AcceptChannel (invalid dust limit)", Tag("mainnet")) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + // we don't want their dust limit to be below 546 + val lowDustLimitSatoshis = 545 + alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUSTLIMIT).getMessage.getBytes("UTF-8"))) + awaitCond(alice.stateName == CLOSED) } - test("recv AcceptChannel (reserve too high)") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - val accept = bob2alice.expectMsgType[AcceptChannel] - // 30% is huge, recommended ratio is 1% - val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).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.getBytes("UTF-8"))) - awaitCond(alice.stateName == CLOSED) - } + test("recv AcceptChannel (to_self_delay too high)") { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + val delayTooHigh = 10000 + alice ! accept.copy(toSelfDelay = delayTooHigh) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelayBlocks).getMessage.getBytes("UTF-8"))) + awaitCond(alice.stateName == CLOSED) } - test("recv AcceptChannel (reserve below dust limit)") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - val accept = bob2alice.expectMsgType[AcceptChannel] - val reserveTooSmall = accept.dustLimitSatoshis - 1 - alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) - val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage.getBytes("UTF-8"))) - awaitCond(alice.stateName == CLOSED) - } + test("recv AcceptChannel (reserve too high)") { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + // 30% is huge, recommended ratio is 1% + val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).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.getBytes("UTF-8"))) + awaitCond(alice.stateName == CLOSED) } - test("recv AcceptChannel (reserve below our dust limit)") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - val accept = bob2alice.expectMsgType[AcceptChannel] - val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent - val reserveTooSmall = open.dustLimitSatoshis - 1 - alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) - val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage.getBytes("UTF-8"))) - awaitCond(alice.stateName == CLOSED) - } + test("recv AcceptChannel (reserve below dust limit)") { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + val reserveTooSmall = accept.dustLimitSatoshis - 1 + alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage.getBytes("UTF-8"))) + awaitCond(alice.stateName == CLOSED) } - test("recv AcceptChannel (dust limit above our reserve)") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - val accept = bob2alice.expectMsgType[AcceptChannel] - val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent - val dustTooBig = open.channelReserveSatoshis + 1 - alice ! accept.copy(dustLimitSatoshis = dustTooBig) - val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage.getBytes("UTF-8"))) - awaitCond(alice.stateName == CLOSED) - } + test("recv AcceptChannel (reserve below our dust limit)") { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent + val reserveTooSmall = open.dustLimitSatoshis - 1 + alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage.getBytes("UTF-8"))) + awaitCond(alice.stateName == CLOSED) } - test("recv Error") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - bob ! Error("00" * 32, "oops".getBytes) - awaitCond(bob.stateName == CLOSED) - } + test("recv AcceptChannel (dust limit above our reserve)") { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent + val dustTooBig = open.channelReserveSatoshis + 1 + alice ! accept.copy(dustLimitSatoshis = dustTooBig) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage.getBytes("UTF-8"))) + awaitCond(alice.stateName == CLOSED) } - test("recv CMD_CLOSE") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - alice ! CMD_CLOSE(None) - awaitCond(alice.stateName == CLOSED) - } + test("recv Error") { f => + import f._ + alice ! Error("00" * 32, "oops".getBytes) + awaitCond(alice.stateName == CLOSED) + } + + test("recv CMD_CLOSE") { f => + import f._ + alice ! CMD_CLOSE(None) + awaitCond(alice.stateName == CLOSED) } } 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 22cce2950..52f963b98 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 @@ -21,22 +21,21 @@ import fr.acinq.bitcoin.Block import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.wire.{AcceptChannel, Error, Init, OpenChannel} +import fr.acinq.eclair.wire.{Error, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe] + case class FixtureParam(bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, bob2blockchain: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) @@ -45,161 +44,147 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, bob2alice.ref, aliceInit) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) - } - test((bob, alice2bob, bob2alice, bob2blockchain)) - } - - test("recv OpenChannel") { case (bob, alice2bob, _, _) => - within(30 seconds) { - alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + withFixture(test.toNoArgTest(FixtureParam(bob, alice2bob, bob2alice, bob2blockchain))) } } - test("recv OpenChannel (invalid chain)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - // using livenet genesis block - val livenetChainHash = Block.LivenetGenesisBlock.hash - bob ! open.copy(chainHash = livenetChainHash) - val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, new InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel") { f => + import f._ + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) } - test("recv OpenChannel (funding too low)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - val lowFundingMsat = 100 - bob ! open.copy(fundingSatoshis = lowFundingMsat) - val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (invalid chain)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + // using livenet genesis block + val livenetChainHash = Block.LivenetGenesisBlock.hash + bob ! open.copy(chainHash = livenetChainHash) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (funding too high)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - val highFundingMsat = 100000000 - bob ! open.copy(fundingSatoshis = highFundingMsat) - val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (funding too low)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val lowFundingMsat = 100 + bob ! open.copy(fundingSatoshis = lowFundingMsat) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (invalid max accepted htlcs)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 - bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) - val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, new InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (funding too high)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val highFundingMsat = 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.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (invalid push_msat)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - val invalidPushMsat = 100000000000L - bob ! open.copy(pushMsat = invalidPushMsat) - val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, new InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, 1000 * open.fundingSatoshis).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (invalid max accepted htlcs)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 + bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (to_self_delay too high)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - val delayTooHigh = 10000 - bob ! open.copy(toSelfDelay = delayTooHigh) - val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelayBlocks).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (invalid push_msat)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val invalidPushMsat = 100000000000L + bob ! open.copy(pushMsat = invalidPushMsat) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, 1000 * open.fundingSatoshis).getMessage.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (reserve too high)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - // 30% is huge, recommended ratio is 1% - val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong - bob ! open.copy(channelReserveSatoshis = reserveTooHigh) - val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, new ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (to_self_delay too high)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val delayTooHigh = 10000 + bob ! open.copy(toSelfDelay = delayTooHigh) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelayBlocks).getMessage.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (fee too low, but still valid)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - // set a very small fee - val tinyFee = 253 - bob ! open.copy(feeratePerKw = tinyFee) - val error = bob2alice.expectMsgType[Error] - // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, "local/remote feerates are too different: remoteFeeratePerKw=253 localFeeratePerKw=10000".getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (reserve too high)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + // 30% is huge, recommended ratio is 1% + val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).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.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (fee below absolute valid minimum)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - // set a very small fee - val tinyFee = 252 - bob ! open.copy(feeratePerKw = tinyFee) - val error = bob2alice.expectMsgType[Error] - // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, "remote fee rate is too small: remoteFeeratePerKw=252".getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (fee too low, but still valid)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + // set a very small fee + val tinyFee = 253 + bob ! open.copy(feeratePerKw = tinyFee) + val error = bob2alice.expectMsgType[Error] + // we check that the error uses the temporary channel id + assert(error === Error(open.temporaryChannelId, "local/remote feerates are too different: remoteFeeratePerKw=253 localFeeratePerKw=10000".getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenChannel (fee below absolute valid minimum)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + // set a very small fee + val tinyFee = 252 + bob ! open.copy(feeratePerKw = tinyFee) + val error = bob2alice.expectMsgType[Error] + // we check that the error uses the temporary channel id + assert(error === Error(open.temporaryChannelId, "remote fee rate is too small: remoteFeeratePerKw=252".getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (reserve below dust)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - val reserveTooSmall = open.dustLimitSatoshis - 1 - bob ! open.copy(channelReserveSatoshis = reserveTooSmall) - val error = bob2alice.expectMsgType[Error] - // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, reserveTooSmall).getMessage.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (reserve below dust)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val reserveTooSmall = open.dustLimitSatoshis - 1 + bob ! open.copy(channelReserveSatoshis = reserveTooSmall) + val error = bob2alice.expectMsgType[Error] + // we check that the error uses the temporary channel id + assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, reserveTooSmall).getMessage.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (toLocal + toRemote below reserve)") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val open = alice2bob.expectMsgType[OpenChannel] - val fundingSatoshis = open.channelReserveSatoshis + 499 - val pushMsat = 500 * 1000 - bob ! open.copy(fundingSatoshis = fundingSatoshis, pushMsat = 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.getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv OpenChannel (toLocal + toRemote below reserve)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val fundingSatoshis = open.channelReserveSatoshis + 499 + val pushMsat = 500 * 1000 + bob ! open.copy(fundingSatoshis = fundingSatoshis, pushMsat = 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.getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv Error") { case (bob, _, _, _) => - within(30 seconds) { - bob ! Error("00" * 32, "oops".getBytes()) - awaitCond(bob.stateName == CLOSED) - } + test("recv Error") { f => + import f._ + bob ! Error("00" * 32, "oops".getBytes()) + awaitCond(bob.stateName == CLOSED) } - test("recv CMD_CLOSE") { case (bob, _, _, _) => - within(30 seconds) { - bob ! CMD_CLOSE(None) - awaitCond(bob.stateName == CLOSED) - } + test("recv CMD_CLOSE") { f => + import f._ + bob ! CMD_CLOSE(None) + awaitCond(bob.stateName == CLOSED) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 5a89800a2..36b4d1a42 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -22,20 +22,19 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) @@ -48,22 +47,20 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - } - test((alice, alice2bob, bob2alice, alice2blockchain)) - } - - test("recv Error") { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - bob ! Error("00" * 32, "oops".getBytes) - awaitCond(bob.stateName == CLOSED) + withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) } } - test("recv CMD_CLOSE") { case (alice, alice2bob, bob2alice, _) => - within(30 seconds) { - alice ! CMD_CLOSE(None) - awaitCond(alice.stateName == CLOSED) - } + test("recv Error") { f => + import f._ + alice ! Error("00" * 32, "oops".getBytes) + awaitCond(alice.stateName == CLOSED) + } + + test("recv CMD_CLOSE") { f => + import f._ + alice ! CMD_CLOSE(None) + awaitCond(alice.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 684be75b8..8d6832ce3 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 @@ -24,21 +24,19 @@ 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 org.junit.runner.RunWith -import org.scalatest.Tag -import org.scalatest.junit.JUnitRunner +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe] + case class FixtureParam(bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, bob2blockchain: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ val (fundingSatoshis, pushMsat) = if (test.tags.contains("funder_below_reserve")) { @@ -56,46 +54,42 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - } - test((bob, alice2bob, bob2alice, bob2blockchain)) - } - - test("recv FundingCreated") { case (bob, alice2bob, bob2alice, bob2blockchain) => - within(30 seconds) { - alice2bob.expectMsgType[FundingCreated] - alice2bob.forward(bob) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) - bob2alice.expectMsgType[FundingSigned] - bob2blockchain.expectMsgType[WatchSpent] - bob2blockchain.expectMsgType[WatchConfirmed] + withFixture(test.toNoArgTest(FixtureParam(bob, alice2bob, bob2alice, bob2blockchain))) } } - test("recv FundingCreated (funder can't pay fees)", Tag("funder_below_reserve")) { case (bob, alice2bob, bob2alice, _) => - within(30 seconds) { - val fees = Transactions.commitWeight * TestConstants.feeratePerKw / 1000 - val reserve = Bob.channelParams.channelReserveSatoshis - val missing = 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".getBytes("UTF-8"))) - awaitCond(bob.stateName == CLOSED) - } + test("recv FundingCreated") { f => + import f._ + alice2bob.expectMsgType[FundingCreated] + alice2bob.forward(bob) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) + bob2alice.expectMsgType[FundingSigned] + bob2blockchain.expectMsgType[WatchSpent] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv Error") { case (bob, _, _, _) => - within(30 seconds) { - bob ! Error("00" * 32, "oops".getBytes) - awaitCond(bob.stateName == CLOSED) - } + 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 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".getBytes("UTF-8"))) + awaitCond(bob.stateName == CLOSED) } - test("recv CMD_CLOSE") { case (bob, _, _, _) => - within(30 seconds) { - bob ! CMD_CLOSE(None) - awaitCond(bob.stateName == CLOSED) - } + test("recv Error") { f => + import f._ + bob ! Error("00" * 32, "oops".getBytes) + awaitCond(bob.stateName == CLOSED) + } + + test("recv CMD_CLOSE") { f => + import f._ + bob ! CMD_CLOSE(None) + awaitCond(bob.stateName == CLOSED) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index f772deba9..aa9f46a7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -24,20 +24,19 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) @@ -52,41 +51,37 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED) - } - test((alice, alice2bob, bob2alice, alice2blockchain)) - } - - test("recv FundingSigned with valid signature") { case (alice, _, bob2alice, alice2blockchain) => - within(30 seconds) { - bob2alice.expectMsgType[FundingSigned] - bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - alice2blockchain.expectMsgType[WatchSpent] - alice2blockchain.expectMsgType[WatchConfirmed] + withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) } } - test("recv FundingSigned with invalid signature") { case (alice, alice2bob, _, _) => - within(30 seconds) { - // sending an invalid sig - alice ! FundingSigned("00" * 32, BinaryData("00" * 64)) - awaitCond(alice.stateName == CLOSED) - alice2bob.expectMsgType[Error] - } + test("recv FundingSigned with valid signature") { f => + import f._ + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + alice2blockchain.expectMsgType[WatchSpent] + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv CMD_CLOSE") { case (alice, _, _, _) => - within(30 seconds) { - alice ! CMD_CLOSE(None) - awaitCond(alice.stateName == CLOSED) - } + test("recv FundingSigned with invalid signature") { f => + import f._ + // sending an invalid sig + alice ! FundingSigned("00" * 32, BinaryData("00" * 64)) + awaitCond(alice.stateName == CLOSED) + alice2bob.expectMsgType[Error] } - test("recv CMD_FORCECLOSE") { case (alice, _, _, _) => - within(30 seconds) { - alice ! CMD_FORCECLOSE - awaitCond(alice.stateName == CLOSED) - } + test("recv CMD_CLOSE") { f => + import f._ + alice ! CMD_CLOSE(None) + awaitCond(alice.stateName == CLOSED) + } + + test("recv CMD_FORCECLOSE") { f => + import f._ + alice ! CMD_FORCECLOSE + awaitCond(alice.stateName == CLOSED) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index b40638e35..eeffb8942 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -25,20 +25,19 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) @@ -57,95 +56,86 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH alice2blockchain.expectMsgType[WatchSpent] alice2blockchain.expectMsgType[WatchConfirmed] awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - } - test((alice, bob, alice2bob, bob2alice, alice2blockchain)) - } - - test("recv FundingLocked") { case (alice, bob, _, bob2alice, _) => - within(30 seconds) { - // make bob send a FundingLocked msg - bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42) - val msg = bob2alice.expectMsgType[FundingLocked] - bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred == Some(msg)) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain))) } } - test("recv BITCOIN_FUNDING_DEPTHOK") { case (alice, _, alice2bob, _, alice2blockchain) => - within(30 seconds) { - alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) - alice2blockchain.expectMsgType[WatchLost] - alice2bob.expectMsgType[FundingLocked] - } + test("recv FundingLocked") { f => + import f._ + // make bob send a FundingLocked msg + bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42) + val msg = bob2alice.expectMsgType[FundingLocked] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(msg)) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) } - test("recv BITCOIN_FUNDING_PUBLISH_FAILED") { case (alice, _, alice2bob, _, _) => - within(30 seconds) { - alice ! BITCOIN_FUNDING_PUBLISH_FAILED - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSED) - } + test("recv BITCOIN_FUNDING_DEPTHOK") { f => + import f._ + alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) + alice2blockchain.expectMsgType[WatchLost] + alice2bob.expectMsgType[FundingLocked] } - test("recv BITCOIN_FUNDING_TIMEOUT") { case (alice, _, alice2bob, _, _) => - within(30 seconds) { - alice ! BITCOIN_FUNDING_TIMEOUT - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == ERR_FUNDING_TIMEOUT) - } + test("recv BITCOIN_FUNDING_PUBLISH_FAILED") { f => + import f._ + alice ! BITCOIN_FUNDING_PUBLISH_FAILED + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_FUNDING_SPENT (remote commit)") { case (alice, bob, _, _, alice2blockchain) => - within(30 seconds) { - // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, tx) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - awaitCond(alice.stateName == CLOSING) - } + test("recv BITCOIN_FUNDING_TIMEOUT") { f => + import f._ + alice ! BITCOIN_FUNDING_TIMEOUT + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == ERR_FUNDING_TIMEOUT) } - test("recv BITCOIN_FUNDING_SPENT (other commit)") { case (alice, _, alice2bob, _, alice2blockchain) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - alice2blockchain.expectMsg(PublishAsap(tx)) - awaitCond(alice.stateName == ERR_INFORMATION_LEAK) - } + test("recv BITCOIN_FUNDING_SPENT (remote commit)") { f => + import f._ + // bob publishes his commitment tx + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, tx) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + awaitCond(alice.stateName == CLOSING) } - test("recv Error") { case (alice, _, _, _, alice2blockchain) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! Error("00" * 32, "oops".getBytes) - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) - } + test("recv BITCOIN_FUNDING_SPENT (other commit)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, Transaction(0, Nil, Nil, 0)) + alice2bob.expectMsgType[Error] + alice2blockchain.expectMsg(PublishAsap(tx)) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } - test("recv CMD_CLOSE") { case (alice, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_CONFIRMED))) - } + test("recv Error") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! Error("00" * 32, "oops".getBytes) + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] // claim-main-delayed + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) } - test("recv CMD_FORCECLOSE") { case (alice, _, _, _, alice2blockchain) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! CMD_FORCECLOSE - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) - } + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_CONFIRMED))) + } + + test("recv CMD_FORCECLOSE") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! CMD_FORCECLOSE + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] // claim-main-delayed + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index d53889e48..f6d9c853f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.channel.states.c -import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Transaction @@ -26,20 +25,19 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, router: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) @@ -66,68 +64,62 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp alice2bob.expectMsgType[FundingLocked] awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) awaitCond(bob.stateName == WAIT_FOR_FUNDING_LOCKED) - } - test((alice, bob, alice2bob, bob2alice, alice2blockchain, router)) - } - - test("recv FundingLocked") { case (alice, _, alice2bob, bob2alice, alice2blockchain, router) => - within(30 seconds) { - bob2alice.expectMsgType[FundingLocked] - bob2alice.forward(alice) - awaitCond(alice.stateName == NORMAL) - bob2alice.expectNoMsg(200 millis) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, router))) } } - test("recv BITCOIN_FUNDING_SPENT (remote commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, router) => - within(30 seconds) { - // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, tx) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - awaitCond(alice.stateName == CLOSING) - } + test("recv FundingLocked") { f => + import f._ + bob2alice.expectMsgType[FundingLocked] + bob2alice.forward(alice) + awaitCond(alice.stateName == NORMAL) + bob2alice.expectNoMsg(200 millis) } - test("recv BITCOIN_FUNDING_SPENT (other commit)") { case (alice, _, alice2bob, _, alice2blockchain, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - awaitCond(alice.stateName == ERR_INFORMATION_LEAK) - } + test("recv BITCOIN_FUNDING_SPENT (remote commit)") { f => + import f._ + // bob publishes his commitment tx + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, tx) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + awaitCond(alice.stateName == CLOSING) } - test("recv Error") { case (alice, _, _, _, alice2blockchain, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! Error("00" * 32, "oops".getBytes) - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) - } + test("recv BITCOIN_FUNDING_SPENT (other commit)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, Transaction(0, Nil, Nil, 0)) + alice2bob.expectMsgType[Error] + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } - test("recv CMD_CLOSE") { case (alice, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_LOCKED))) - } + test("recv Error") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! Error("00" * 32, "oops".getBytes) + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) } - test("recv CMD_FORCECLOSE") { case (alice, _, _, _, alice2blockchain, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! CMD_FORCECLOSE - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) - } + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_LOCKED))) + } + + test("recv CMD_FORCECLOSE") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx + alice ! CMD_FORCECLOSE + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) } } 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 75e006fea..1d888b1b3 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 @@ -32,2033 +32,1926 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.Tag -import org.scalatest.junit.JUnitRunner +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ within(30 seconds) { reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer, test.tags) awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer))) } - test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)) } - test("recv CMD_ADD_HTLC (empty origin)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val sender = TestProbe() - val h = BinaryData("42" * 32) + test("recv CMD_ADD_HTLC (empty origin)") { f => + import f._ + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val sender = TestProbe() + val h = BinaryData("42" * 32) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + sender.expectMsg("ok") + val htlc = alice2bob.expectMsgType[UpdateAddHtlc] + assert(htlc.id == 0 && htlc.paymentHash == h) + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localNextHtlcId = 1, + localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil), + originChannels = Map(0L -> Local(Some(sender.ref))) + ))) + } + + test("recv CMD_ADD_HTLC (incrementing ids)") { f => + import f._ + val sender = TestProbe() + val h = BinaryData("42" * 32) + for (i <- 0 until 10) { sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] - assert(htlc.id == 0 && htlc.paymentHash == h) - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localNextHtlcId = 1, - localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil), - originChannels = Map(0L -> Local(Some(sender.ref))) - ))) + assert(htlc.id == i && htlc.paymentHash == h) } } - test("recv CMD_ADD_HTLC (incrementing ids)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val h = BinaryData("42" * 32) - for (i <- 0 until 10) { - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) - sender.expectMsg("ok") - val htlc = alice2bob.expectMsgType[UpdateAddHtlc] - assert(htlc.id == i && htlc.paymentHash == h) - } - } + test("recv CMD_ADD_HTLC (relayed htlc)") { f => + import f._ + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val sender = TestProbe() + val h = BinaryData("42" * 32) + val originHtlc = UpdateAddHtlc(channelId = "42" * 32, id = 5656, amountMsat = 50000000, expiry = 400144, paymentHash = h, onionRoutingPacket = "00" * 1254) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.expiry - 7, upstream_opt = Some(originHtlc)) + sender.send(alice, cmd) + sender.expectMsg("ok") + val htlc = alice2bob.expectMsgType[UpdateAddHtlc] + assert(htlc.id == 0 && htlc.paymentHash == h) + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localNextHtlcId = 1, + localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil), + originChannels = Map(0L -> Relayed(originHtlc.channelId, originHtlc.id, originHtlc.amountMsat, htlc.amountMsat)) + ))) } - test("recv CMD_ADD_HTLC (relayed htlc)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val sender = TestProbe() - val h = BinaryData("42" * 32) - val originHtlc = UpdateAddHtlc(channelId = "42" * 32, id = 5656, amountMsat = 50000000, expiry = 400144, paymentHash = h, onionRoutingPacket = "00" * 1254) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.expiry - 7, upstream_opt = Some(originHtlc)) - sender.send(alice, cmd) - sender.expectMsg("ok") - val htlc = alice2bob.expectMsgType[UpdateAddHtlc] - assert(htlc.id == 0 && htlc.paymentHash == h) - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localNextHtlcId = 1, - localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil), - originChannels = Map(0L -> Relayed(originHtlc.channelId, originHtlc.id, originHtlc.amountMsat, htlc.amountMsat)) - ))) - } + test("recv CMD_ADD_HTLC (invalid payment hash)") { f => + import f._ + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val sender = TestProbe() + val add = CMD_ADD_HTLC(500000000, "11" * 42, expiry = 400144) + sender.send(alice, add) + val error = InvalidPaymentHash(channelId(alice)) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (invalid payment hash)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 42, expiry = 400144) - sender.send(alice, add) - val error = InvalidPaymentHash(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (expiry too small)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val currentBlockCount = Globals.blockCount.get + val expiryTooSmall = currentBlockCount + 3 + val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = expiryTooSmall) + 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(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (expiry too small)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val currentBlockCount = Globals.blockCount.get - val expiryTooSmall = currentBlockCount + 3 - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = expiryTooSmall) - 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(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (expiry too big)") { f => + import f._ + val sender = TestProbe() + 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, "11" * 32, expiry = expiryTooBig) + 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(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (expiry too big)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - 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, "11" * 32, expiry = expiryTooBig) - 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(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (value too small)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val add = CMD_ADD_HTLC(50, "11" * 32, 400144) + sender.send(alice, add) + val error = HtlcValueTooSmall(channelId(alice), 1000, 50) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (value too small)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(50, "11" * 32, 400144) - sender.send(alice, add) - val error = HtlcValueTooSmall(channelId(alice), 1000, 50) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (insufficient funds)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val add = CMD_ADD_HTLC(Int.MaxValue, "11" * 32, 400144) + sender.send(alice, add) + val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (insufficient funds)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(Int.MaxValue, "11" * 32, 400144) - sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CMD_ADD_HTLC(500000000, "11" * 32, 400144)) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + sender.send(alice, CMD_ADD_HTLC(200000000, "22" * 32, 400144)) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + sender.send(alice, CMD_ADD_HTLC(67600000, "33" * 32, 400144)) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + val add = CMD_ADD_HTLC(1000000, "44" * 32, 400144) + sender.send(alice, add) + val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(500000000, "11" * 32, 400144)) + test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CMD_ADD_HTLC(300000000, "11" * 32, 400144)) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + sender.send(alice, CMD_ADD_HTLC(300000000, "22" * 32, 400144)) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + val add = CMD_ADD_HTLC(500000000, "33" * 32, 400144) + sender.send(alice, add) + val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) + } + + test("recv CMD_ADD_HTLC (over max inflight htlc value)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + val add = CMD_ADD_HTLC(151000000, "11" * 32, 400144) + sender.send(bob, add) + val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) + sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + bob2alice.expectNoMsg(200 millis) + } + + test("recv CMD_ADD_HTLC (over max accepted htlcs)") { f => + import f._ + val sender = TestProbe() + 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, "11" * 32, 400144)) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(200000000, "22" * 32, 400144)) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(67600000, "33" * 32, 400144)) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(1000000, "44" * 32, 400144) - sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) } + val add = CMD_ADD_HTLC(10000000, "33" * 32, 400144) + sender.send(alice, add) + val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(300000000, "11" * 32, 400144)) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(300000000, "22" * 32, 400144)) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(500000000, "33" * 32, 400144) - sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (over capacity)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "11" * 32, 400144) + sender.send(alice, add1) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + // this is over channel-capacity + val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "22" * 32, 400144) + sender.send(alice, add2) + val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (over max inflight htlc value)") { case (_, bob, _, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(151000000, "11" * 32, 400144) - sender.send(bob, add) - val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) - sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - bob2alice.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (after having sent Shutdown)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg("ok") + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && !alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isDefined) + + // actual test starts here + val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144) + sender.send(alice, add) + val error = NoMoreHtlcsClosingInProgress(channelId(alice)) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_ADD_HTLC (over max accepted htlcs)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - 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, "11" * 32, 400144)) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - } - val add = CMD_ADD_HTLC(10000000, "33" * 32, 400144) - sender.send(alice, add) - val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC (after having received Shutdown)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + // let's make alice send an htlc + val add1 = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144) + 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, "22" * 32, expiry = 300000) + // messages cross + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + sender.send(alice, add2) + val error = NoMoreHtlcsClosingInProgress(channelId(alice)) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) } - test("recv CMD_ADD_HTLC (over capacity)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "11" * 32, 400144) - sender.send(alice, add1) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - // this is over channel-capacity - val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "22" * 32, 400144) - sender.send(alice, add2) - val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv UpdateAddHtlc") { f => + import f._ + val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] + val htlc = UpdateAddHtlc("00" * 32, 0, 150000, BinaryData("42" * 32), 400144, defaultOnion) + bob ! htlc + awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) } - test("recv CMD_ADD_HTLC (after having sent Shutdown)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg("ok") - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && !alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isDefined) - - // actual test starts here - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144) - sender.send(alice, add) - val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv UpdateAddHtlc (unexpected id)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val htlc = UpdateAddHtlc("00" * 32, 42, 150000, BinaryData("42" * 32), 400144, defaultOnion) + bob ! htlc.copy(id = 0) + bob ! htlc.copy(id = 1) + bob ! htlc.copy(id = 2) + bob ! htlc.copy(id = 3) + bob ! htlc.copy(id = 42) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage) + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv CMD_ADD_HTLC (after having received Shutdown)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - // let's make alice send an htlc - val add1 = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144) - 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, "22" * 32, expiry = 300000) - // messages cross - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - bob2alice.expectMsgType[Shutdown] - bob2alice.forward(alice) - sender.send(alice, add2) - val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) - } + test("recv UpdateAddHtlc (invalid payment hash)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val htlc = UpdateAddHtlc("00" * 32, 0, 150000, "11" * 42, 400144, defaultOnion) + alice2bob.forward(bob, htlc) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === InvalidPaymentHash(channelId(bob)).getMessage) + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateAddHtlc") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - val htlc = UpdateAddHtlc("00" * 32, 0, 150000, BinaryData("42" * 32), 400144, defaultOnion) - bob ! htlc - awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) - } + test("recv UpdateAddHtlc (value too small)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val htlc = UpdateAddHtlc("00" * 32, 0, 150, BinaryData("42" * 32), expiry = 400144, defaultOnion) + alice2bob.forward(bob, htlc) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === HtlcValueTooSmall(channelId(bob), minimum = 1000, actual = 150).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateAddHtlc (unexpected id)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc("00" * 32, 42, 150000, BinaryData("42" * 32), 400144, defaultOnion) - bob ! htlc.copy(id = 0) - bob ! htlc.copy(id = 1) - bob ! htlc.copy(id = 2) - bob ! htlc.copy(id = 3) - bob ! htlc.copy(id = 42) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage) - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateAddHtlc (insufficient funds)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val htlc = UpdateAddHtlc("00" * 32, 0, Long.MaxValue, BinaryData("42" * 32), 400144, defaultOnion) + alice2bob.forward(bob, htlc) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateAddHtlc (invalid payment hash)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc("00" * 32, 0, 150000, "11" * 42, 400144, defaultOnion) - alice2bob.forward(bob, htlc) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === InvalidPaymentHash(channelId(bob)).getMessage) - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } + 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("00" * 32, 0, 400000000, "11" * 32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 200000000, "22" * 32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 167600000, "33" * 32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 3, 10000000, "44" * 32, 400144, defaultOnion)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateAddHtlc (value too small)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc("00" * 32, 0, 150, BinaryData("42" * 32), expiry = 400144, defaultOnion) - alice2bob.forward(bob, htlc) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === HtlcValueTooSmall(channelId(bob), minimum = 1000, actual = 150).getMessage) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } + 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("00" * 32, 0, 300000000, "11" * 32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 300000000, "22" * 32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 500000000, "33" * 32, 400144, defaultOnion)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateAddHtlc (insufficient funds)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc("00" * 32, 0, Long.MaxValue, BinaryData("42" * 32), 400144, defaultOnion) - alice2bob.forward(bob, htlc) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).getMessage) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } + 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("00" * 32, 0, 151000000, "11" * 32, 400144, defaultOnion)) + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000).getMessage) + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 0, 400000000, "11" * 32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 200000000, "22" * 32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 167600000, "33" * 32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 3, 10000000, "44" * 32, 400144, defaultOnion)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).getMessage) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] + test("recv UpdateAddHtlc (over max accepted htlcs)") { f => + import f._ + 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("00" * 32, i, 1000000, "11" * 32, 400144, defaultOnion)) } + alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 30, 1000000, "11" * 32, 400144, defaultOnion)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 0, 300000000, "11" * 32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 300000000, "22" * 32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 500000000, "33" * 32, 400144, defaultOnion)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).getMessage) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv CMD_SIGN") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + val commitSig = alice2bob.expectMsgType[CommitSig] + assert(commitSig.htlcSignatures.size == 1) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) } - test("recv UpdateAddHtlc (over max inflight htlc value)") { case (alice, _, alice2bob, _, alice2blockchain, _, relayer) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(alice, UpdateAddHtlc("00" * 32, 0, 151000000, "11" * 32, 400144, defaultOnion)) - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000).getMessage) - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } + test("recv CMD_SIGN (two identical htlcs in each direction)") { f => + import f._ + val sender = TestProbe() + val add = CMD_ADD_HTLC(10000000, "11" * 32, 400144) + sender.send(alice, add) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + sender.send(alice, add) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + + crossSign(alice, bob, alice2bob, bob2alice) + + sender.send(bob, add) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateAddHtlc] + bob2alice.forward(alice) + sender.send(bob, add) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateAddHtlc] + bob2alice.forward(alice) + + // actual test starts here + sender.send(bob, CMD_SIGN) + sender.expectMsg("ok") + val commitSig = bob2alice.expectMsgType[CommitSig] + assert(commitSig.htlcSignatures.toSet.size == 4) } - test("recv UpdateAddHtlc (over max accepted htlcs)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - 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("00" * 32, i, 1000000, "11" * 32, 400144, defaultOnion)) - } - alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 30, 1000000, "11" * 32, 400144, defaultOnion)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CMD_SIGN") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - val commitSig = alice2bob.expectMsgType[CommitSig] - assert(commitSig.htlcSignatures.size == 1) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) - } - } - - test("recv CMD_SIGN (two identical htlcs in each direction)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, "11" * 32, 400144) - sender.send(alice, add) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - sender.send(alice, add) + test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => + import f._ + val sender = TestProbe() + val add = CMD_ADD_HTLC(10000000, "11" * 32, 400144) + 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.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) + } + // actual test starts here + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + val commitSig = alice2bob.expectMsgType[CommitSig] + assert(commitSig.htlcSignatures.toSet.size == htlcCount) + alice2bob.forward(bob) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == htlcCount) + val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs + val amounts = htlcTxs.map(_.txinfo.tx.txOut.head.amount.toLong) + assert(amounts === amounts.sorted) + } + test("recv CMD_SIGN (no changes)") { f => + import f._ + val sender = TestProbe() + sender.send(alice, CMD_SIGN) + sender.expectNoMsg(1 second) // just ignored + //sender.expectMsg("cannot sign when there are no changes") + } + + 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) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) + val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get + assert(waitForRevocation.reSignAsap === false) + + // actual test starts here + sender.send(alice, CMD_SIGN) + sender.expectNoMsg(300 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation)) + } + + 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) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) + val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get + assert(waitForRevocation.reSignAsap === false) + + // actual test starts here + val (r2, htlc2) = addHtlc(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))) + } + + test("recv CommitSig (one htlc received)") { f => + import f._ + val sender = TestProbe() + + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + + // actual test begins + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + + bob2alice.expectMsgType[RevokeAndAck] + // bob replies immediately with a signature + bob2alice.expectMsgType[CommitSig] + + 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.remoteChanges.acked.size == 0) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.signed.size == 1) + } + + test("recv CommitSig (one htlc sent)") { f => + import f._ + val sender = TestProbe() + + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + + // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + 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) + } + + 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 (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + + val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + + val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + + val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + + // actual test begins + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 3) + } + + test("recv CommitSig (only fee update)") { f => + import f._ + val sender = TestProbe() + + sender.send(alice, CMD_UPDATE_FEE(TestConstants.feeratePerKw + 1000, commit = false)) + sender.expectMsg("ok") + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + + // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) + alice2bob.expectMsgType[UpdateFee] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + } + + test("recv CommitSig (two htlcs received with same r)") { f => + import f._ + val sender = TestProbe() + val r = BinaryData("42" * 32) + val h: BinaryData = Crypto.sha256(r) + + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + sender.expectMsg("ok") + val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + sender.expectMsg("ok") + val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.proposed == htlc1 :: htlc2 :: Nil) + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + + 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.publishableTxs.commitTx.tx.txOut.count(_.amount == Satoshi(50000)) == 2) + } + + test("recv CommitSig (no changes)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + // signature is invalid but it doesn't matter + sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv CommitSig (invalid signature)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + + // actual test begins + sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data).startsWith("invalid commitment signature")) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv CommitSig (bad htlc sig count)") { f => + import f._ + val sender = TestProbe() + + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + val commitSig = alice2bob.expectMsgType[CommitSig] + + // actual test begins + val badCommitSig = commitSig.copy(htlcSignatures = commitSig.htlcSignatures ::: commitSig.htlcSignatures) + sender.send(bob, badCommitSig) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv CommitSig (invalid htlc sig)") { f => + import f._ + val sender = TestProbe() + + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + val commitSig = alice2bob.expectMsgType[CommitSig] + + // actual test begins + val badCommitSig = commitSig.copy(htlcSignatures = commitSig.signature :: Nil) + sender.send(bob, badCommitSig) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data).startsWith("invalid htlc signature")) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + + test("recv RevokeAndAck (one htlc sent)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + + // actual test begins + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localChanges.acked.size == 1) + } + + test("recv RevokeAndAck (one htlc received)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + // actual test begins + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + } + + 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 (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + + val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + + val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + + val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + // actual test begins + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.index == 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.spec.htlcs.size == 7) + } + + test("recv RevokeAndAck (with reSignAsap=true)") { f => + import f._ + val sender = TestProbe() + val (r1, htlc1) = addHtlc(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) + sender.send(alice, CMD_SIGN) + sender.expectNoMsg(300 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap === true) + + // actual test starts here + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + alice2bob.expectMsgType[CommitSig] + } + + test("recv RevokeAndAck (invalid preimage)") { f => + 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) + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + + // actual test begins + bob2alice.expectMsgType[RevokeAndAck] + sender.send(alice, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv RevokeAndAck (unexpectedly)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + sender.send(alice, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv CMD_FULFILL_HTLC") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) + sender.expectMsg("ok") + val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] + awaitCond(bob.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill)))) + } + + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => + import f._ + val sender = TestProbe() + val r: BinaryData = "11" * 32 + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + + sender.send(bob, CMD_FULFILL_HTLC(42, r)) + sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) + assert(initialState == bob.stateData) + } + + test("recv CMD_FULFILL_HTLC (invalid preimage)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + sender.send(bob, CMD_FULFILL_HTLC(htlc.id, "00" * 32)) + sender.expectMsg(Failure(InvalidHtlcPreimage(channelId(bob), 0))) + assert(initialState == bob.stateData) + } + + test("recv UpdateFulfillHtlc") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) + sender.expectMsg("ok") + val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] + + // actual test begins + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + bob2alice.forward(alice) + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)))) + } + + test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + + // actual test begins + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + sender.send(alice, UpdateFulfillHtlc("00" * 32, htlc.id, r)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv UpdateFulfillHtlc (unknown htlc id)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(alice, UpdateFulfillHtlc("00" * 32, 42, "00" * 32)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv UpdateFulfillHtlc (invalid preimage)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + + // actual test begins + sender.send(alice, UpdateFulfillHtlc("00" * 32, htlc.id, "00" * 32)) + relayer.expectMsgType[ForwardAdd] + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv CMD_FAIL_HTLC") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) + sender.expectMsg("ok") + val fail = bob2alice.expectMsgType[UpdateFailHtlc] + awaitCond(bob.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) + } + + test("recv CMD_FAIL_HTLC (unknown htlc id)") { f => + import f._ + val sender = TestProbe() + val r: BinaryData = "11" * 32 + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + + sender.send(bob, CMD_FAIL_HTLC(42, Right(PermanentChannelFailure))) + sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) + assert(initialState == bob.stateData) + } + + test("recv CMD_FAIL_MALFORMED_HTLC") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.expectMsg("ok") + val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] + awaitCond(bob.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) + } + + test("recv CMD_FAIL_MALFORMED_HTLC (unknown htlc id)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, FailureMessageCodecs.BADONION)) + sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) + assert(initialState == bob.stateData) + } + + test("recv CMD_FAIL_HTLC (invalid failure_code)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, 42)) + sender.expectMsg(Failure(InvalidFailureCode(channelId(bob)))) + assert(initialState == bob.stateData) + } + + test("recv UpdateFailHtlc") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) + sender.expectMsg("ok") + val fail = bob2alice.expectMsgType[UpdateFailHtlc] + + // actual test begins + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + bob2alice.forward(alice) + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) + } + + test("recv UpdateFailMalformedHtlc") { f => + import f._ + val sender = TestProbe() + + // Alice sends an HTLC to Bob, which they both sign + val (r, htlc) = addHtlc(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] + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.expectMsg("ok") + val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] + bob2alice.forward(alice) + + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) + + sender.send(bob, CMD_SIGN) + val sig = bob2alice.expectMsgType[CommitSig] + // Bob should not have the htlc in its remote commit anymore + assert(sig.htlcSignatures.isEmpty) + + // and Alice should accept this signature + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + } + + test("recv UpdateFailMalformedHtlc (invalid failure_code)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val fail = UpdateFailMalformedHtlc("00" * 32, htlc.id, Crypto.sha256(htlc.onionRoutingPacket), 42) + sender.send(alice, fail) + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data) === InvalidFailureCode("00" * 32).getMessage) + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv UpdateFailHtlc (sender has not signed htlc)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + + // actual test begins + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + sender.send(alice, UpdateFailHtlc("00" * 32, htlc.id, "00" * 152)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv UpdateFailHtlc (unknown htlc id)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(alice, UpdateFailHtlc("00" * 32, 42, "00" * 152)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv CMD_UPDATE_FEE") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CMD_UPDATE_FEE(20000)) + sender.expectMsg("ok") + val fee = alice2bob.expectMsgType[UpdateFee] + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee)))) + } + + test("recv CMD_UPDATE_FEE (two in a row)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CMD_UPDATE_FEE(20000)) + sender.expectMsg("ok") + val fee1 = alice2bob.expectMsgType[UpdateFee] + sender.send(alice, CMD_UPDATE_FEE(30000)) + sender.expectMsg("ok") + val fee2 = alice2bob.expectMsgType[UpdateFee] + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee2)))) + } + + test("recv CMD_UPDATE_FEE (when fundee)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + sender.send(bob, CMD_UPDATE_FEE(20000)) + sender.expectMsg(Failure(FundeeCannotSendUpdateFee(channelId(bob)))) + assert(initialState == bob.stateData) + } + + test("recv UpdateFee") { f => + import f._ + val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] + val fee1 = UpdateFee("00" * 32, 12000) + bob ! fee1 + val fee2 = UpdateFee("00" * 32, 14000) + bob ! fee2 + awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee2), remoteNextHtlcId = 0))) + } + + test("recv UpdateFee (two in a row)") { f => + import f._ + val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] + val fee = UpdateFee("00" * 32, 12000) + bob ! fee + awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) + } + + test("recv UpdateFee (when sender is not funder)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(alice, UpdateFee("00" * 32, 12000)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv UpdateFee (sender can't afford it)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + val fee = UpdateFee("00" * 32, 100000000) + // we first update the global variable so that we don't trigger a 'fee too different' error + Globals.feeratesPerKw.set(FeeratesPerKw.single(fee.feeratePerKw)) + sender.send(bob, fee) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === CannotAffordFees(channelId(bob), missingSatoshis = 71620000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + //bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees) + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv UpdateFee (local/remote feerates are too different)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(bob, UpdateFee("00" * 32, 85000)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=85000 localFeeratePerKw=10000") + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv UpdateFee (remote feerate is too small)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(bob, UpdateFee("00" * 32, 252)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === "remote fee rate is too small: remoteFeeratePerKw=252") + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv CMD_UPDATE_RELAY_FEE ") { f => + import f._ + val sender = TestProbe() + val newFeeBaseMsat = TestConstants.Alice.nodeParams.feeBaseMsat * 2 + val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.feeProportionalMillionth * 2 + sender.send(alice, CMD_UPDATE_RELAY_FEE(newFeeBaseMsat, newFeeProportionalMillionth)) + sender.expectMsg("ok") + + val localUpdate = relayer.expectMsgType[LocalChannelUpdate] + assert(localUpdate.channelUpdate.feeBaseMsat == newFeeBaseMsat) + assert(localUpdate.channelUpdate.feeProportionalMillionths == newFeeProportionalMillionth) + relayer.expectNoMsg(1 seconds) + } + + test("recv CMD_CLOSE (no pending htlcs)") { f => + import f._ + val sender = TestProbe() + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg("ok") + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateName == NORMAL) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) + } + + test("recv CMD_CLOSE (with unacked sent htlcs)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(CannotCloseWithUnsignedOutgoingHtlcs(channelId(bob)))) + } + + test("recv CMD_CLOSE (with invalid final script)") { f => + import f._ + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(Some(BinaryData("00112233445566778899")))) + sender.expectMsg(Failure(InvalidFinalScript(channelId(alice)))) + } + + test("recv CMD_CLOSE (with signed sent htlcs)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg("ok") + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateName == NORMAL) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) + } + + test("recv CMD_CLOSE (two in a row)") { f => + import f._ + val sender = TestProbe() + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg("ok") + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateName == NORMAL) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) + } + + test("recv CMD_CLOSE (while waiting for a RevokeAndAck)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + // actual test begins + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg("ok") + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateName == NORMAL) + } + + test("recv Shutdown (no pending htlcs)") { f => + import f._ + val sender = TestProbe() + sender.send(alice, Shutdown("00" * 32, Bob.channelParams.defaultFinalScriptPubKey)) + alice2bob.expectMsgType[Shutdown] + alice2bob.expectMsgType[ClosingSigned] + awaitCond(alice.stateName == NEGOTIATING) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId) + } + + test("recv Shutdown (with unacked sent htlcs)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + sender.send(bob, CMD_CLOSE(None)) + bob2alice.expectMsgType[Shutdown] + // actual test begins + bob2alice.forward(alice) + // alice sends a new sig + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // bob replies with a revocation + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + // as soon as alice as received the revocation, she will send her shutdown message + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateName == SHUTDOWN) + // channel should be advertised as down + assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId) + } + + test("recv Shutdown (with unacked received htlcs)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + // actual test begins + sender.send(bob, Shutdown("00" * 32, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)) + bob2alice.expectMsgType[Error] + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + awaitCond(bob.stateName == CLOSING) + } + + test("recv Shutdown (with invalid final script)") { f => + import f._ + val sender = TestProbe() + sender.send(bob, Shutdown("00" * 32, BinaryData("00112233445566778899"))) + bob2alice.expectMsgType[Error] + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + awaitCond(bob.stateName == CLOSING) + } + + 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) + crossSign(alice, bob, alice2bob, bob2alice) + sender.send(bob, CMD_CLOSE(None)) + bob2alice.expectMsgType[Shutdown] + // actual test begins + sender.send(bob, Shutdown("00" * 32, BinaryData("00112233445566778899"))) + bob2alice.expectMsgType[Error] + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + awaitCond(bob.stateName == CLOSING) + } + + test("recv Shutdown (with signed htlcs)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + sender.send(bob, Shutdown("00" * 32, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)) + bob2alice.expectMsgType[Shutdown] + awaitCond(bob.stateName == SHUTDOWN) + } + + test("recv Shutdown (while waiting for a RevokeAndAck)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + sender.send(bob, CMD_CLOSE(None)) + bob2alice.expectMsgType[Shutdown] + // actual test begins + bob2alice.forward(alice) + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateName == SHUTDOWN) + } + + test("recv Shutdown (while waiting for a RevokeAndAck with pending outgoing htlc)") { f => + import f._ + val sender = TestProbe() + // let's make bob send a Shutdown message + 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) + // 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) + // actual test begins + // alice eventually gets bob's shutdown + bob2alice.forward(alice) + // alice can't do anything for now other than waiting for bob to send the revocation + alice2bob.expectNoMsg() + // bob sends the revocation + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + // bob will also sign back + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + // then alice can sign the 2nd htlc + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // and reply to bob's first signature + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + // bob replies with the 2nd revocation + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + // then alice can send her shutdown + alice2bob.expectMsgType[Shutdown] + awaitCond(alice.stateName == SHUTDOWN) + // note: bob will sign back a second time, but that is out of our scope + } + + test("recv CurrentBlockCount (no htlc timed out)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CurrentBlockCount(400143)) + awaitCond(alice.stateData == initialState) + } + + test("recv CurrentBlockCount (an htlc timed out)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // actual test begins + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx + sender.send(alice, CurrentBlockCount(400145)) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) + + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed + val watch = alice2blockchain.expectMsgType[WatchConfirmed] + assert(watch.event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) + } + + test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val event = CurrentFeerates(FeeratesPerKw.single(20000)) + sender.send(alice, event) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) + } + + test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => + import f._ + val sender = TestProbe() + val event = CurrentFeerates(FeeratesPerKw.single(10010)) + sender.send(alice, event) + alice2bob.expectNoMsg(500 millis) + } + + test("recv CurrentFeerate (when fundee, commit-fee/network-fee are close)") { f => + import f._ + val sender = TestProbe() + val event = CurrentFeerates(FeeratesPerKw.single(11000)) + sender.send(bob, event) + bob2alice.expectNoMsg(500 millis) + } + + test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { f => + import f._ + val sender = TestProbe() + val event = CurrentFeerates(FeeratesPerKw.single(100)) + sender.send(bob, event) + bob2alice.expectMsgType[Error] + bob2blockchain.expectMsgType[PublishAsap] // commit tx + bob2blockchain.expectMsgType[PublishAsap] // main delayed + bob2blockchain.expectMsgType[WatchConfirmed] + awaitCond(bob.stateName == CLOSING) + } + + test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { f => + 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) + crossSign(alice, bob, alice2bob, bob2alice) + fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) + fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) + + // at this point here is the situation from alice pov and what she should do when bob publishes his commit tx: + // balances : + // alice's balance : 449 999 990 => nothing to do + // bob's balance : 95 000 000 => nothing to do + // htlcs : + // alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend + // alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend + // alice -> bob : 10 (dust) => won't appear in the commitment tx + // bob -> alice : 50 000 000 (alice has the preimage) => spend immediately using the preimage + // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout + + // bob publishes his current commit tx + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + assert(bobCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + + // in response to that, alice publishes its claim txes + val claimTxes = for (i <- 0 until 4) yield alice2blockchain.expectMsgType[PublishAsap].tx + // in addition to its main output, alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage + val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { + assert(claimHtlcTx.txIn.size == 1) + assert(claimHtlcTx.txOut.size == 1) + Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimHtlcTx.txOut(0).amount + }).sum + // at best we have a little less than 450 000 + 250 000 + 100 000 + 50 000 = 850 000 (because fees) + assert(amountClaimed == Satoshi(814840)) + + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) // claim-main + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + alice2blockchain.expectNoMsg(1 second) + + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 1) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) + } + + test("recv BITCOIN_FUNDING_SPENT (their *next* commit w/ htlc)") { f => + 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) + crossSign(alice, bob, alice2bob, bob2alice) + fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) + fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) + // alice sign but we intercept bob's revocation + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + + // as far as alice knows, bob currently has two valid unrevoked commitment transactions + + // at this point here is the situation from bob's pov with the latest sig received from alice, + // and what alice should do when bob publishes his commit tx: + // balances : + // alice's balance : 499 999 990 => nothing to do + // bob's balance : 95 000 000 => nothing to do + // htlcs : + // alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend + // alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend + // alice -> bob : 10 (dust) => won't appear in the commitment tx + // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout + + // bob publishes his current commit tx + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + assert(bobCommitTx.txOut.size == 5) // two main outputs and 3 pending htlcs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + + // in response to that, alice publishes its claim txes + val claimTxes = for (i <- 0 until 3) yield alice2blockchain.expectMsgType[PublishAsap].tx + // in addition to its main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage + val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { + assert(claimHtlcTx.txIn.size == 1) + assert(claimHtlcTx.txOut.size == 1) + Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimHtlcTx.txOut(0).amount + }).sum + // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) + assert(amountClaimed == Satoshi(822280)) + + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) // claim-main + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + alice2blockchain.expectNoMsg(1 second) + + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcSuccessTxs.size == 0) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) + } + + test("recv BITCOIN_FUNDING_SPENT (revoked commit)") { f => + import f._ + val sender = TestProbe() + + // initially we have : + // alice = 800 000 + // bob = 200 000 + def send(): Transaction = { + // alice sends 8 000 sat + val (r, htlc) = addHtlc(10000000, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - sender.send(bob, add) - sender.expectMsg("ok") - bob2alice.expectMsgType[UpdateAddHtlc] - bob2alice.forward(alice) - sender.send(bob, add) - sender.expectMsg("ok") - bob2alice.expectMsgType[UpdateAddHtlc] - bob2alice.forward(alice) - - // actual test starts here - sender.send(bob, CMD_SIGN) - sender.expectMsg("ok") - val commitSig = bob2alice.expectMsgType[CommitSig] - assert(commitSig.htlcSignatures.toSet.size == 4) - } - } - - test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, "11" * 32, 400144) - 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.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - } - // actual test starts here - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - val commitSig = alice2bob.expectMsgType[CommitSig] - assert(commitSig.htlcSignatures.toSet.size == htlcCount) - alice2bob.forward(bob) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == htlcCount) - val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs - val amounts = htlcTxs.map(_.txinfo.tx.txOut.head.amount.toLong) - assert(amounts === amounts.sorted) - } - } - - test("recv CMD_SIGN (no changes)") { case (alice, _, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, CMD_SIGN) - sender.expectNoMsg(1 second) // just ignored - //sender.expectMsg("cannot sign when there are no changes") - } - } - - test("recv CMD_SIGN (while waiting for RevokeAndAck (no pending changes)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(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] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) - val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) - - // actual test starts here - sender.send(alice, CMD_SIGN) - sender.expectNoMsg(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation)) - } - } - - test("recv CMD_SIGN (while waiting for RevokeAndAck (with pending changes)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r1, htlc1) = addHtlc(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] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) - val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) - - // actual test starts here - val (r2, htlc2) = addHtlc(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))) - } - } - - test("recv CommitSig (one htlc received)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - - // actual test begins - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - - bob2alice.expectMsgType[RevokeAndAck] - // bob replies immediately with a signature - bob2alice.expectMsgType[CommitSig] - - 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.remoteChanges.acked.size == 0) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.signed.size == 1) - } - } - - test("recv CommitSig (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - - // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - - 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) - } - } - - test("recv CommitSig (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) - - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) - - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) - - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) - - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) - - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) - - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - - // actual test begins - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 3) - } - } - - test("recv CommitSig (only fee update)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - - sender.send(alice, CMD_UPDATE_FEE(TestConstants.feeratePerKw + 1000, commit = false)) - sender.expectMsg("ok") - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - - // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) - alice2bob.expectMsgType[UpdateFee] - alice2bob.forward(bob) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - } - } - - test("recv CommitSig (two htlcs received with same r)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val r = BinaryData("42" * 32) - val h: BinaryData = Crypto.sha256(r) - - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) - sender.expectMsg("ok") - val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) - sender.expectMsg("ok") - val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.proposed == htlc1 :: htlc2 :: Nil) - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - - 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.publishableTxs.commitTx.tx.txOut.count(_.amount == Satoshi(50000)) == 2) - } - } - - test("recv CommitSig (no changes)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - // signature is invalid but it doesn't matter - sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) - bob2alice.expectMsgType[Error] - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CommitSig (invalid signature)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - - // actual test begins - sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data).startsWith("invalid commitment signature")) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CommitSig (bad htlc sig count)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - val commitSig = alice2bob.expectMsgType[CommitSig] - - // actual test begins - val badCommitSig = commitSig.copy(htlcSignatures = commitSig.htlcSignatures ::: commitSig.htlcSignatures) - sender.send(bob, badCommitSig) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CommitSig (invalid htlc sig)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - val commitSig = alice2bob.expectMsgType[CommitSig] - - // actual test begins - val badCommitSig = commitSig.copy(htlcSignatures = commitSig.signature :: Nil) - sender.send(bob, badCommitSig) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data).startsWith("invalid htlc signature")) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - - test("recv RevokeAndAck (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - - // actual test begins - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localChanges.acked.size == 1) - } - } - - test("recv RevokeAndAck (one htlc received)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) - - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - - // actual test begins - alice2bob.expectMsgType[RevokeAndAck] - alice2bob.forward(bob) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) - } - } - - test("recv RevokeAndAck (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) - - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) - - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) - - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) - - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) - - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) - - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) - - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - - // actual test begins - alice2bob.expectMsgType[RevokeAndAck] - alice2bob.forward(bob) - - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.index == 1) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.spec.htlcs.size == 7) - } - } - - test("recv RevokeAndAck (with reSignAsap=true)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r1, htlc1) = addHtlc(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) - sender.send(alice, CMD_SIGN) - sender.expectNoMsg(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap === true) - - // actual test starts here - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - alice2bob.expectMsgType[CommitSig] - } - } - - test("recv RevokeAndAck (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - - // actual test begins - bob2alice.expectMsgType[RevokeAndAck] - sender.send(alice, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv RevokeAndAck (unexpectedly)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) - sender.send(alice, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CMD_FULFILL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) - sender.expectMsg("ok") - val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] - awaitCond(bob.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill)))) - } - } - - test("recv CMD_FULFILL_HTLC (unknown htlc id)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val r: BinaryData = "11" * 32 - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - - sender.send(bob, CMD_FULFILL_HTLC(42, r)) - sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) - assert(initialState == bob.stateData) - } - } - - test("recv CMD_FULFILL_HTLC (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FULFILL_HTLC(htlc.id, "00" * 32)) - sender.expectMsg(Failure(InvalidHtlcPreimage(channelId(bob), 0))) - assert(initialState == bob.stateData) - } - } - - test("recv UpdateFulfillHtlc") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) - sender.expectMsg("ok") - val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] - - // actual test begins - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - bob2alice.forward(alice) - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)))) - } - } - - test("recv UpdateFulfillHtlc (sender has not signed htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - - // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - sender.send(alice, UpdateFulfillHtlc("00" * 32, htlc.id, r)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv UpdateFulfillHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _, relayer) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(alice, UpdateFulfillHtlc("00" * 32, 42, "00" * 32)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv UpdateFulfillHtlc (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - - // actual test begins - sender.send(alice, UpdateFulfillHtlc("00" * 32, htlc.id, "00" * 32)) - relayer.expectMsgType[ForwardAdd] - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CMD_FAIL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) - sender.expectMsg("ok") - val fail = bob2alice.expectMsgType[UpdateFailHtlc] - awaitCond(bob.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) - } - } - - test("recv CMD_FAIL_HTLC (unknown htlc id)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val r: BinaryData = "11" * 32 - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - - sender.send(bob, CMD_FAIL_HTLC(42, Right(PermanentChannelFailure))) - sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) - assert(initialState == bob.stateData) - } - } - - test("recv CMD_FAIL_MALFORMED_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) - sender.expectMsg("ok") - val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] - awaitCond(bob.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) - } - } - - test("recv CMD_FAIL_MALFORMED_HTLC (unknown htlc id)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, FailureMessageCodecs.BADONION)) - sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) - assert(initialState == bob.stateData) - } - } - - test("recv CMD_FAIL_HTLC (invalid failure_code)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, 42)) - sender.expectMsg(Failure(InvalidFailureCode(channelId(bob)))) - assert(initialState == bob.stateData) - } - } - - test("recv UpdateFailHtlc") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) - sender.expectMsg("ok") - val fail = bob2alice.expectMsgType[UpdateFailHtlc] - - // actual test begins - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - bob2alice.forward(alice) - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) - } - } - - test("recv UpdateFailMalformedHtlc") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - - // Alice sends an HTLC to Bob, which they both sign - val (r, htlc) = addHtlc(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] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) - sender.expectMsg("ok") - val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] - bob2alice.forward(alice) - - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) - - sender.send(bob, CMD_SIGN) - val sig = bob2alice.expectMsgType[CommitSig] - // Bob should not have the htlc in its remote commit anymore - assert(sig.htlcSignatures.isEmpty) - - // and Alice should accept this signature - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - } - } - - test("recv UpdateFailMalformedHtlc (invalid failure_code)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val fail = UpdateFailMalformedHtlc("00" * 32, htlc.id, Crypto.sha256(htlc.onionRoutingPacket), 42) - sender.send(alice, fail) - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data) === InvalidFailureCode("00" * 32).getMessage) - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv UpdateFailHtlc (sender has not signed htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - - // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - sender.send(alice, UpdateFailHtlc("00" * 32, htlc.id, "00" * 152)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv UpdateFailHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _, relayer) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(alice, UpdateFailHtlc("00" * 32, 42, "00" * 152)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CMD_UPDATE_FEE") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_UPDATE_FEE(20000)) - sender.expectMsg("ok") - val fee = alice2bob.expectMsgType[UpdateFee] - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee)))) - } - } - - test("recv CMD_UPDATE_FEE (two in a row)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_UPDATE_FEE(20000)) - sender.expectMsg("ok") - val fee1 = alice2bob.expectMsgType[UpdateFee] - sender.send(alice, CMD_UPDATE_FEE(30000)) - sender.expectMsg("ok") - val fee2 = alice2bob.expectMsgType[UpdateFee] - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee2)))) - } - } - - test("recv CMD_UPDATE_FEE (when fundee)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_UPDATE_FEE(20000)) - sender.expectMsg(Failure(FundeeCannotSendUpdateFee(channelId(bob)))) - assert(initialState == bob.stateData) - } - } - - test("recv UpdateFee") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - val fee1 = UpdateFee("00" * 32, 12000) - bob ! fee1 - val fee2 = UpdateFee("00" * 32, 14000) - bob ! fee2 - awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee2), remoteNextHtlcId = 0))) - } - } - - test("recv UpdateFee (two in a row)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - val fee = UpdateFee("00" * 32, 12000) - bob ! fee - awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) - } - } - - test("recv UpdateFee (when sender is not funder)") { case (alice, _, alice2bob, _, alice2blockchain, _, relayer) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(alice, UpdateFee("00" * 32, 12000)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv UpdateFee (sender can't afford it)") { case (_, bob, _, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - val fee = UpdateFee("00" * 32, 100000000) - // we first update the global variable so that we don't trigger a 'fee too different' error - Globals.feeratesPerKw.set(FeeratesPerKw.single(fee.feeratePerKw)) - sender.send(bob, fee) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === CannotAffordFees(channelId(bob), missingSatoshis = 71620000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx - //bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees) - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv UpdateFee (local/remote feerates are too different)") { case (_, bob, _, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(bob, UpdateFee("00" * 32, 85000)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=85000 localFeeratePerKw=10000") - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv UpdateFee (remote feerate is too small)") { case (_, bob, _, bob2alice, _, bob2blockchain, relayer) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(bob, UpdateFee("00" * 32, 252)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === "remote fee rate is too small: remoteFeeratePerKw=252") - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv CMD_UPDATE_RELAY_FEE ") { case (alice, bob, alice2bob, bob2alice, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - val newFeeBaseMsat = TestConstants.Alice.nodeParams.feeBaseMsat * 2 - val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.feeProportionalMillionth * 2 - sender.send(alice, CMD_UPDATE_RELAY_FEE(newFeeBaseMsat, newFeeProportionalMillionth)) - sender.expectMsg("ok") - - val localUpdate = relayer.expectMsgType[LocalChannelUpdate] - assert(localUpdate.channelUpdate.feeBaseMsat == newFeeBaseMsat) - assert(localUpdate.channelUpdate.feeProportionalMillionths == newFeeProportionalMillionth) - relayer.expectNoMsg(1 seconds) - } - } - - test("recv CMD_CLOSE (no pending htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg("ok") - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == NORMAL) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) - } - } - - test("recv CMD_CLOSE (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(CannotCloseWithUnsignedOutgoingHtlcs(channelId(bob)))) - } - } - - test("recv CMD_CLOSE (with invalid final script)") { case (alice, _, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, CMD_CLOSE(Some(BinaryData("00112233445566778899")))) - sender.expectMsg(Failure(InvalidFinalScript(channelId(alice)))) - } - } - - test("recv CMD_CLOSE (with signed sent htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg("ok") - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == NORMAL) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) - } - } - - test("recv CMD_CLOSE (two in a row)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg("ok") - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == NORMAL) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) - } - } - - test("recv CMD_CLOSE (while waiting for a RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - // actual test begins - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg("ok") - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == NORMAL) - } - } - - test("recv Shutdown (no pending htlcs)") { case (alice, _, alice2bob, _, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, Shutdown("00" * 32, Bob.channelParams.defaultFinalScriptPubKey)) - alice2bob.expectMsgType[Shutdown] - alice2bob.expectMsgType[ClosingSigned] - awaitCond(alice.stateName == NEGOTIATING) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId) - } - } - - test("recv Shutdown (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - sender.send(bob, CMD_CLOSE(None)) - bob2alice.expectMsgType[Shutdown] - // actual test begins - bob2alice.forward(alice) - // alice sends a new sig - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - // bob replies with a revocation - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - // as soon as alice as received the revocation, she will send her shutdown message - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == SHUTDOWN) - // channel should be advertised as down - assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId) - } - } - - test("recv Shutdown (with unacked received htlcs)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - // actual test begins - sender.send(bob, Shutdown("00" * 32, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)) - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - awaitCond(bob.stateName == CLOSING) - } - } - - test("recv Shutdown (with invalid final script)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(bob, Shutdown("00" * 32, BinaryData("00112233445566778899"))) - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - awaitCond(bob.stateName == CLOSING) - } - } - - test("recv Shutdown (with invalid final script and signed htlcs, in response to a Shutdown)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - sender.send(bob, CMD_CLOSE(None)) - bob2alice.expectMsgType[Shutdown] - // actual test begins - sender.send(bob, Shutdown("00" * 32, BinaryData("00112233445566778899"))) - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - awaitCond(bob.stateName == CLOSING) - } - } - - test("recv Shutdown (with signed htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - sender.send(bob, Shutdown("00" * 32, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)) - bob2alice.expectMsgType[Shutdown] - awaitCond(bob.stateName == SHUTDOWN) - } - } - - test("recv Shutdown (while waiting for a RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - sender.send(bob, CMD_CLOSE(None)) - bob2alice.expectMsgType[Shutdown] - // actual test begins - bob2alice.forward(alice) - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == SHUTDOWN) - } - } - - test("recv Shutdown (while waiting for a RevokeAndAck with pending outgoing htlc)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - // let's make bob send a Shutdown message - 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) - // 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) - // actual test begins - // alice eventually gets bob's shutdown - bob2alice.forward(alice) - // alice can't do anything for now other than waiting for bob to send the revocation - alice2bob.expectNoMsg() - // bob sends the revocation - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - // bob will also sign back - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - // then alice can sign the 2nd htlc - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - // and reply to bob's first signature - alice2bob.expectMsgType[RevokeAndAck] - alice2bob.forward(bob) - // bob replies with the 2nd revocation - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - // then alice can send her shutdown - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == SHUTDOWN) - // note: bob will sign back a second time, but that is out of our scope - } - } - - test("recv CurrentBlockCount (no htlc timed out)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CurrentBlockCount(400143)) - awaitCond(alice.stateData == initialState) - } - } - - test("recv CurrentBlockCount (an htlc timed out)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - // actual test begins - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx - sender.send(alice, CurrentBlockCount(400145)) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) - - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed - val watch = alice2blockchain.expectMsgType[WatchConfirmed] - assert(watch.event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) - } - } - - test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val event = CurrentFeerates(FeeratesPerKw.single(20000)) - sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) - } - } - - test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val event = CurrentFeerates(FeeratesPerKw.single(10010)) - sender.send(alice, event) - alice2bob.expectNoMsg(500 millis) - } - } - - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are close)") { case (_, bob, _, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val event = CurrentFeerates(FeeratesPerKw.single(11000)) - sender.send(bob, event) - bob2alice.expectNoMsg(500 millis) - } - } - - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - val event = CurrentFeerates(FeeratesPerKw.single(100)) - sender.send(bob, event) - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishAsap] // commit tx - bob2blockchain.expectMsgType[PublishAsap] // main delayed - bob2blockchain.expectMsgType[WatchConfirmed] - awaitCond(bob.stateName == CLOSING) - } - } - - test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _) => - within(30 seconds) { - 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) - crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) - - // at this point here is the situation from alice pov and what she should do when bob publishes his commit tx: - // balances : - // alice's balance : 449 999 990 => nothing to do - // bob's balance : 95 000 000 => nothing to do - // htlcs : - // alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend - // alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend - // alice -> bob : 10 (dust) => won't appear in the commitment tx - // bob -> alice : 50 000 000 (alice has the preimage) => spend immediately using the preimage - // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout - - // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - assert(bobCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - - // in response to that, alice publishes its claim txes - val claimTxes = for (i <- 0 until 4) yield alice2blockchain.expectMsgType[PublishAsap].tx - // in addition to its main output, alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { - assert(claimHtlcTx.txIn.size == 1) - assert(claimHtlcTx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut(0).amount - }).sum - // at best we have a little less than 450 000 + 250 000 + 100 000 + 50 000 = 850 000 (because fees) - assert(amountClaimed == Satoshi(814840)) - - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) // claim-main - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - alice2blockchain.expectNoMsg(1 second) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 1) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) - } - } - - test("recv BITCOIN_FUNDING_SPENT (their *next* commit w/ htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _) => - within(30 seconds) { - 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) - crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) - // alice sign but we intercept bob's revocation - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] - - // as far as alice knows, bob currently has two valid unrevoked commitment transactions - - // at this point here is the situation from bob's pov with the latest sig received from alice, - // and what alice should do when bob publishes his commit tx: - // balances : - // alice's balance : 499 999 990 => nothing to do - // bob's balance : 95 000 000 => nothing to do - // htlcs : - // alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend - // alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend - // alice -> bob : 10 (dust) => won't appear in the commitment tx - // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout - - // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - assert(bobCommitTx.txOut.size == 5) // two main outputs and 3 pending htlcs - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - - // in response to that, alice publishes its claim txes - val claimTxes = for (i <- 0 until 3) yield alice2blockchain.expectMsgType[PublishAsap].tx - // in addition to its main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { - assert(claimHtlcTx.txIn.size == 1) - assert(claimHtlcTx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut(0).amount - }).sum - // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) - assert(amountClaimed == Satoshi(822280)) - - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) // claim-main - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - alice2blockchain.expectNoMsg(1 second) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcSuccessTxs.size == 0) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) - } - } - - test("recv BITCOIN_FUNDING_SPENT (revoked commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - - // initially we have : - // alice = 800 000 - // bob = 200 000 - def send(): Transaction = { - // alice sends 8 000 sat - val (r, htlc) = addHtlc(10000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - } - - val txs = for (i <- 0 until 10) yield send() - // bob now has 10 spendable tx, 9 of them being revoked - - // let's say that bob published this tx - val revokedTx = txs(3) - // channel state for this revoked tx is as follows: - // alice = 760 000 - // bob = 200 000 - // a->b = 10 000 - // a->b = 10 000 - // a->b = 10 000 - // a->b = 10 000 - // two main outputs + 4 htlc - assert(revokedTx.txOut.size == 6) - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx) - alice2bob.expectMsgType[Error] - - val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx - val htlcPenaltyTxs = for (i <- 0 until 4) yield alice2blockchain.expectMsgType[PublishAsap].tx - assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx)) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty - // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) - htlcPenaltyTxs.foreach(htlcPenaltyTx => assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT)) - alice2blockchain.expectNoMsg(1 second) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - - // two main outputs are 760 000 and 200 000 - assert(mainTx.txOut(0).amount == Satoshi(741490)) - assert(mainPenaltyTx.txOut(0).amount == Satoshi(195150)) - assert(htlcPenaltyTxs(0).txOut(0).amount == Satoshi(4530)) - assert(htlcPenaltyTxs(1).txOut(0).amount == Satoshi(4530)) - assert(htlcPenaltyTxs(2).txOut(0).amount == Satoshi(4530)) - assert(htlcPenaltyTxs(3).txOut(0).amount == Satoshi(4530)) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - - } - } - - test("recv BITCOIN_FUNDING_SPENT (revoked commit with identical htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - - // initially we have : - // alice = 800 000 - // bob = 200 000 - - val add = CMD_ADD_HTLC(10000000, "11" * 32, 400144) - sender.send(alice, add) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - sender.send(alice, add) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - - crossSign(alice, bob, alice2bob, bob2alice) - // bob will publish this tx after it is revoked - val revokedTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - - sender.send(alice, add) - sender.expectMsg("ok") - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) - - crossSign(alice, bob, alice2bob, bob2alice) - - // channel state for this revoked tx is as follows: - // alice = 780 000 - // bob = 200 000 - // a->b = 10 000 - // a->b = 10 000 - assert(revokedTx.txOut.size == 4) - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx) - alice2bob.expectMsgType[Error] - - val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx - val htlcPenaltyTxs = for (i <- 0 until 2) yield alice2blockchain.expectMsgType[PublishAsap].tx - // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx)) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty - htlcPenaltyTxs.foreach(htlcPenaltyTx => assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT)) - alice2blockchain.expectNoMsg(1 second) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - } - } - - test("recv Error") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - 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) - crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) - - // at this point here is the situation from alice pov and what she should do when she publishes his commit tx: - // balances : - // alice's balance : 449 999 990 => nothing to do - // bob's balance : 95 000 000 => nothing to do - // htlcs : - // alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend using 2nd stage htlc-timeout - // alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend using 2nd stage htlc-timeout - // alice -> bob : 10 (dust) => won't appear in the commitment tx - // bob -> alice : 50 000 000 (alice has the preimage) => spend immediately using the preimage using htlc-success - // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout - - // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice ! Error("00" * 32, "oops".getBytes()) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) - assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs - - // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the htlc - // so we expect 7 transactions: - // - 1 tx to claim the main delayed output - // - 3 txes for each htlc - // - 3 txes for each delayed output of the claimed htlc - val claimTxs = for (i <- 0 until 7) yield alice2blockchain.expectMsgType[PublishAsap].tx - - // the main delayed output spends the commitment transaction - Transaction.correctlySpends(claimTxs(0), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // 2nd stage transactions spend the commitment transaction - Transaction.correctlySpends(claimTxs(1), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(claimTxs(2), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(claimTxs(3), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // 3rd stage transactions spend their respective HTLC-Success/HTLC-Timeout transactions - Transaction.correctlySpends(claimTxs(4), claimTxs(1) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(claimTxs(5), claimTxs(2) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(claimTxs(6), claimTxs(3) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(0))) // main-delayed - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(4))) // htlc-delayed - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(5))) // htlc-delayed - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(6))) // htlc-delayed - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - alice2blockchain.expectNoMsg(1 second) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) - val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(localCommitPublished.commitTx == aliceCommitTx) - assert(localCommitPublished.htlcSuccessTxs.size == 1) - assert(localCommitPublished.htlcTimeoutTxs.size == 2) - assert(localCommitPublished.claimHtlcDelayedTxs.size == 3) - } - } - - test("recv BITCOIN_FUNDING_DEEPLYBURIED", Tag("channels_public")) { case (alice, _, alice2bob, _, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMsg(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) - // we don't re-publish the same channel_update if there was no change - relayer.expectNoMsg(1 second) - } - } - - test("recv BITCOIN_FUNDING_DEEPLYBURIED (short channel id changed)", Tag("channels_public")) { case (alice, _, alice2bob, _, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400001, 22)) - val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMsg(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) - assert(relayer.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - relayer.expectNoMsg(1 second) - } - } - - test("recv BITCOIN_FUNDING_DEEPLYBURIED (private channel)") { case (alice, _, alice2bob, _, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) - // we don't re-publish the same channel_update if there was no change - relayer.expectNoMsg(1 second) - } - } - - test("recv BITCOIN_FUNDING_DEEPLYBURIED (private channel, short channel id changed)") { case (alice, _, alice2bob, _, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400001, 22)) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) - // LocalChannelUpdate should not be published - assert(relayer.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - relayer.expectNoMsg(1 second) - } - } - - test("recv AnnouncementSignatures", Tag("channels_public")) { case (alice, bob, alice2bob, bob2alice, _, _, relayer) => - within(30 seconds) { - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] - import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) - // actual test starts here - bob2alice.forward(alice) - awaitCond({ - val normal = alice.stateData.asInstanceOf[DATA_NORMAL] - normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement == Some(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId - }) - assert(relayer.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === Some(channelAnn)) - } - } - - test("recv AnnouncementSignatures (re-send)", Tag("channels_public")) { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) - val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) - val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] - import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) - bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) - - // actual test starts here - // simulate bob re-sending its sigs - bob2alice.send(alice, annSigsA) - // alice re-sends her sigs - alice2bob.expectMsg(annSigsA) - } - } - - test("recv TickRefreshChannelUpdate", Tag("channels_public")) { case (alice, bob, _, bob2alice, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - bob2alice.expectMsgType[AnnouncementSignatures] - bob2alice.forward(alice) - val update1 = relayer.expectMsgType[LocalChannelUpdate] - - // actual test starts here - Thread.sleep(1100) - sender.send(alice, TickRefreshChannelUpdate) - val update2 = relayer.expectMsgType[LocalChannelUpdate] - assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) - } - } - - test("recv INPUT_DISCONNECTED", Tag("channels_public")) { case (alice, bob, _, bob2alice, _, _, relayer) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) - bob2alice.expectMsgType[AnnouncementSignatures] - bob2alice.forward(alice) - val update1 = relayer.expectMsgType[LocalChannelUpdate] - assert(Announcements.isEnabled(update1.channelUpdate.flags) == true) - - // actual test starts here - Thread.sleep(1100) - sender.send(alice, INPUT_DISCONNECTED) - val update2 = relayer.expectMsgType[LocalChannelUpdate] - assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) - assert(Announcements.isEnabled(update2.channelUpdate.flags) == false) - awaitCond(alice.stateName == OFFLINE) - } + bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + } + + val txs = for (i <- 0 until 10) yield send() + // bob now has 10 spendable tx, 9 of them being revoked + + // let's say that bob published this tx + val revokedTx = txs(3) + // channel state for this revoked tx is as follows: + // alice = 760 000 + // bob = 200 000 + // a->b = 10 000 + // a->b = 10 000 + // a->b = 10 000 + // a->b = 10 000 + // two main outputs + 4 htlc + assert(revokedTx.txOut.size == 6) + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx) + alice2bob.expectMsgType[Error] + + val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenaltyTxs = for (i <- 0 until 4) yield alice2blockchain.expectMsgType[PublishAsap].tx + assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx)) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty + // let's make sure that htlc-penalty txs each spend a different output + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) + htlcPenaltyTxs.foreach(htlcPenaltyTx => assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT)) + alice2blockchain.expectNoMsg(1 second) + + Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + + // two main outputs are 760 000 and 200 000 + assert(mainTx.txOut(0).amount == Satoshi(741490)) + assert(mainPenaltyTx.txOut(0).amount == Satoshi(195150)) + assert(htlcPenaltyTxs(0).txOut(0).amount == Satoshi(4530)) + assert(htlcPenaltyTxs(1).txOut(0).amount == Satoshi(4530)) + assert(htlcPenaltyTxs(2).txOut(0).amount == Satoshi(4530)) + assert(htlcPenaltyTxs(3).txOut(0).amount == Satoshi(4530)) + + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + + } + + test("recv BITCOIN_FUNDING_SPENT (revoked commit with identical htlcs)") { f => + import f._ + val sender = TestProbe() + + // initially we have : + // alice = 800 000 + // bob = 200 000 + + val add = CMD_ADD_HTLC(10000000, "11" * 32, 400144) + sender.send(alice, add) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + sender.send(alice, add) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + + crossSign(alice, bob, alice2bob, bob2alice) + // bob will publish this tx after it is revoked + val revokedTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + + sender.send(alice, add) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + + crossSign(alice, bob, alice2bob, bob2alice) + + // channel state for this revoked tx is as follows: + // alice = 780 000 + // bob = 200 000 + // a->b = 10 000 + // a->b = 10 000 + assert(revokedTx.txOut.size == 4) + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx) + alice2bob.expectMsgType[Error] + + val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenaltyTxs = for (i <- 0 until 2) yield alice2blockchain.expectMsgType[PublishAsap].tx + // let's make sure that htlc-penalty txs each spend a different output + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx)) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty + htlcPenaltyTxs.foreach(htlcPenaltyTx => assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT)) + alice2blockchain.expectNoMsg(1 second) + + Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + } + + 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) + crossSign(alice, bob, alice2bob, bob2alice) + fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) + fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) + + // at this point here is the situation from alice pov and what she should do when she publishes his commit tx: + // balances : + // alice's balance : 449 999 990 => nothing to do + // bob's balance : 95 000 000 => nothing to do + // htlcs : + // alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend using 2nd stage htlc-timeout + // alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend using 2nd stage htlc-timeout + // alice -> bob : 10 (dust) => won't appear in the commitment tx + // bob -> alice : 50 000 000 (alice has the preimage) => spend immediately using the preimage using htlc-success + // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout + + // an error occurs and alice publishes her commit tx + val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + alice ! Error("00" * 32, "oops".getBytes()) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) + assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs + + // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the htlc + // so we expect 7 transactions: + // - 1 tx to claim the main delayed output + // - 3 txes for each htlc + // - 3 txes for each delayed output of the claimed htlc + val claimTxs = for (i <- 0 until 7) yield alice2blockchain.expectMsgType[PublishAsap].tx + + // the main delayed output spends the commitment transaction + Transaction.correctlySpends(claimTxs(0), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + // 2nd stage transactions spend the commitment transaction + Transaction.correctlySpends(claimTxs(1), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimTxs(2), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimTxs(3), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + // 3rd stage transactions spend their respective HTLC-Success/HTLC-Timeout transactions + Transaction.correctlySpends(claimTxs(4), claimTxs(1) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimTxs(5), claimTxs(2) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimTxs(6), claimTxs(3) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(0))) // main-delayed + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(4))) // htlc-delayed + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(5))) // htlc-delayed + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(6))) // htlc-delayed + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + alice2blockchain.expectNoMsg(1 second) + + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) + val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get + assert(localCommitPublished.commitTx == aliceCommitTx) + assert(localCommitPublished.htlcSuccessTxs.size == 1) + assert(localCommitPublished.htlcTimeoutTxs.size == 2) + assert(localCommitPublished.claimHtlcDelayedTxs.size == 3) + } + + test("recv BITCOIN_FUNDING_DEEPLYBURIED", Tag("channels_public")) { f => + import f._ + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] + // public channel: we don't send the channel_update directly to the peer + alice2bob.expectNoMsg(1 second) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) + // we don't re-publish the same channel_update if there was no change + relayer.expectNoMsg(1 second) + } + + test("recv BITCOIN_FUNDING_DEEPLYBURIED (short channel id changed)", Tag("channels_public")) { f => + import f._ + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400001, 22)) + val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] + // public channel: we don't send the channel_update directly to the peer + alice2bob.expectNoMsg(1 second) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) + assert(relayer.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) + relayer.expectNoMsg(1 second) + } + + test("recv BITCOIN_FUNDING_DEEPLYBURIED (private channel)") { f => + import f._ + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + // private channel: we send the channel_update directly to the peer + val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) + // we don't re-publish the same channel_update if there was no change + relayer.expectNoMsg(1 second) + } + + test("recv BITCOIN_FUNDING_DEEPLYBURIED (private channel, short channel id changed)") { f => + import f._ + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400001, 22)) + // private channel: we send the channel_update directly to the peer + val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true) + // LocalChannelUpdate should not be published + assert(relayer.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) + relayer.expectNoMsg(1 second) + } + + test("recv AnnouncementSignatures", Tag("channels_public")) { f => + import f._ + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] + sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] + import initialState.commitments.{localParams, remoteParams} + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + // actual test starts here + bob2alice.forward(alice) + awaitCond({ + val normal = alice.stateData.asInstanceOf[DATA_NORMAL] + normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement == Some(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId + }) + assert(relayer.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === Some(channelAnn)) + } + + test("recv AnnouncementSignatures (re-send)", Tag("channels_public")) { f => + import f._ + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) + val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] + sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) + val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] + import initialState.commitments.{localParams, remoteParams} + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) + + // actual test starts here + // simulate bob re-sending its sigs + bob2alice.send(alice, annSigsA) + // alice re-sends her sigs + alice2bob.expectMsg(annSigsA) + } + + test("recv TickRefreshChannelUpdate", Tag("channels_public")) { f => + import f._ + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + val update1 = relayer.expectMsgType[LocalChannelUpdate] + + // actual test starts here + Thread.sleep(1100) + sender.send(alice, TickRefreshChannelUpdate) + val update2 = relayer.expectMsgType[LocalChannelUpdate] + assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) + } + + test("recv INPUT_DISCONNECTED", Tag("channels_public")) { f => + import f._ + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) + bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + val update1 = relayer.expectMsgType[LocalChannelUpdate] + assert(Announcements.isEnabled(update1.channelUpdate.flags) == true) + + // actual test starts here + Thread.sleep(1100) + sender.send(alice, INPUT_DISCONNECTED) + val update2 = relayer.expectMsgType[LocalChannelUpdate] + assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) + assert(Announcements.isEnabled(update2.channelUpdate.flags) == false) + awaitCond(alice.stateName == OFFLINE) } } 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 5434b265e..0d678ef76 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 @@ -26,34 +26,34 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ within(30 seconds) { reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer) awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer))) } } /** * This test checks the case where a disconnection occurs *right before* the counterparty receives a new sig */ - test("re-send update+sig after first commitment") { case (alice, bob, alice2bob, bob2alice, _, _, _) => + test("re-send update+sig after first commitment") { f => + import f._ val sender = TestProbe() sender.send(alice, CMD_ADD_HTLC(1000000, BinaryData("42" * 32), 400144)) @@ -75,7 +75,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) @@ -129,7 +129,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { /** * This test checks the case where a disconnection occurs *right after* the counterparty receives a new sig */ - test("re-send lost revocation") { case (alice, bob, alice2bob, bob2alice, _, _, _) => + test("re-send lost revocation") { f => + import f._ val sender = TestProbe() sender.send(alice, CMD_ADD_HTLC(1000000, BinaryData("42" * 32), 400144)) @@ -158,7 +159,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) // a didn't receive the sig @@ -189,7 +190,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } - test("discover that we have a revoked commitment") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => + test("discover that we have a revoked commitment") { f => + import f._ val sender = TestProbe() val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) @@ -242,7 +244,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } - test("discover that they have a more recent commit than the one we know") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => + test("discover that they have a more recent commit than the one we know") { f => + import f._ val sender = TestProbe() // we start by storing the current state @@ -293,7 +296,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } - test("counterparty lies about having a more recent commitment") { case (alice, bob, alice2bob, bob2alice, _, _, _) => + test("counterparty lies about having a more recent commitment") { f => + import f._ val sender = TestProbe() // we simulate a disconnection @@ -319,8 +323,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(new String(error.data) === InvalidRevokedCommitProof(channelId(alice), 0, 42, ba_reestablish_forged.yourLastPerCommitmentSecret.get).getMessage) } - test("change relay fee while offline") { case (alice, bob, alice2bob, bob2alice, _, _, relayer) => - + test("change relay fee while offline") { f => + import f._ val sender = TestProbe() // we simulate a disconnection 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 2a9ec4ba3..e4b34d658 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 @@ -24,24 +24,23 @@ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} -import fr.acinq.eclair.payment.{ForwardAdd, Local, PaymentLifecycle, _} +import fr.acinq.eclair.payment.{ForwardAdd, Local, PaymentLifecycle} import fr.acinq.eclair.router.Hop -import fr.acinq.eclair.wire.{ChannelUpdate, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ within(30 seconds) { @@ -91,711 +90,666 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.forward(alice) awaitCond(alice.stateName == SHUTDOWN) awaitCond(bob.stateName == SHUTDOWN) - test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer))) } } - test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) - sender.send(alice, add) - val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC") { f => + import f._ + val sender = TestProbe() + val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) + sender.send(alice, add) + val error = ChannelUnavailable(channelId(alice)) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_FULFILL_HTLC") { case (_, bob, _, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) - sender.expectMsg("ok") - val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] - awaitCond(bob.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill)))) - } + test("recv CMD_FULFILL_HTLC") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) + sender.expectMsg("ok") + val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] + awaitCond(bob.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill)))) } - test("recv CMD_FULFILL_HTLC (unknown htlc id)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FULFILL_HTLC(42, "12" * 32)) - sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) - assert(initialState == bob.stateData) - } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FULFILL_HTLC(42, "12" * 32)) + sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) + assert(initialState == bob.stateData) } - test("recv CMD_FULFILL_HTLC (invalid preimage)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FULFILL_HTLC(1, "00" * 32)) - sender.expectMsg(Failure(InvalidHtlcPreimage(channelId(bob), 1))) - assert(initialState == bob.stateData) - } + test("recv CMD_FULFILL_HTLC (invalid preimage)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FULFILL_HTLC(1, "00" * 32)) + sender.expectMsg(Failure(InvalidHtlcPreimage(channelId(bob), 1))) + assert(initialState == bob.stateData) } - test("recv UpdateFulfillHtlc") { case (alice, _, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val fulfill = UpdateFulfillHtlc("00" * 32, 0, "11" * 32) - sender.send(alice, fulfill) - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments == initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill))) - } + test("recv UpdateFulfillHtlc") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] + val fulfill = UpdateFulfillHtlc("00" * 32, 0, "11" * 32) + sender.send(alice, fulfill) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments == initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill))) } - test("recv UpdateFulfillHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - val fulfill = UpdateFulfillHtlc("00" * 32, 42, "00" * 32) - sender.send(alice, fulfill) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - alice2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFulfillHtlc (unknown htlc id)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + val fulfill = UpdateFulfillHtlc("00" * 32, 42, "00" * 32) + sender.send(alice, fulfill) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateFulfillHtlc (invalid preimage)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(alice, UpdateFulfillHtlc("00" * 32, 42, "00" * 32)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - alice2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFulfillHtlc (invalid preimage)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(alice, UpdateFulfillHtlc("00" * 32, 42, "00" * 32)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv CMD_FAIL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FAIL_HTLC(1, Right(PermanentChannelFailure))) - sender.expectMsg("ok") - val fail = bob2alice.expectMsgType[UpdateFailHtlc] - awaitCond(bob.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) - } + test("recv CMD_FAIL_HTLC") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FAIL_HTLC(1, Right(PermanentChannelFailure))) + sender.expectMsg("ok") + val fail = bob2alice.expectMsgType[UpdateFailHtlc] + awaitCond(bob.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) } - test("recv CMD_FAIL_HTLC (unknown htlc id)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FAIL_HTLC(42, Right(PermanentChannelFailure))) - sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) - assert(initialState == bob.stateData) - } + test("recv CMD_FAIL_HTLC (unknown htlc id)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FAIL_HTLC(42, Right(PermanentChannelFailure))) + sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) + assert(initialState == bob.stateData) } - test("recv CMD_FAIL_MALFORMED_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(1, Crypto.sha256(BinaryData.empty), FailureMessageCodecs.BADONION)) - sender.expectMsg("ok") - val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] - awaitCond(bob.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) - } + test("recv CMD_FAIL_MALFORMED_HTLC") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(1, Crypto.sha256(BinaryData.empty), FailureMessageCodecs.BADONION)) + sender.expectMsg("ok") + val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] + awaitCond(bob.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) } - test("recv CMD_FAIL_MALFORMED_HTLC (unknown htlc id)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, FailureMessageCodecs.BADONION)) - sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) - assert(initialState == bob.stateData) - } + test("recv CMD_FAIL_MALFORMED_HTLC (unknown htlc id)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, FailureMessageCodecs.BADONION)) + sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) + assert(initialState == bob.stateData) } - test("recv CMD_FAIL_HTLC (invalid failure_code)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, 42)) - sender.expectMsg(Failure(InvalidFailureCode(channelId(bob)))) - assert(initialState == bob.stateData) - } + test("recv CMD_FAIL_HTLC (invalid failure_code)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, "00" * 32, 42)) + sender.expectMsg(Failure(InvalidFailureCode(channelId(bob)))) + assert(initialState == bob.stateData) } - test("recv UpdateFailHtlc") { case (alice, _, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val fail = UpdateFailHtlc("00" * 32, 1, "00" * 152) - sender.send(alice, fail) - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments == initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail))) - } + test("recv UpdateFailHtlc") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] + val fail = UpdateFailHtlc("00" * 32, 1, "00" * 152) + sender.send(alice, fail) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments == initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail))) } - test("recv UpdateFailHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(alice, UpdateFailHtlc("00" * 32, 42, "00" * 152)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - alice2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFailHtlc (unknown htlc id)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(alice, UpdateFailHtlc("00" * 32, 42, "00" * 152)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateFailMalformedHtlc") { case (alice, _, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val fail = UpdateFailMalformedHtlc("00" * 32, 1, Crypto.sha256(BinaryData.empty), FailureMessageCodecs.BADONION) - sender.send(alice, fail) - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments == initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail))) - } + test("recv UpdateFailMalformedHtlc") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] + val fail = UpdateFailMalformedHtlc("00" * 32, 1, Crypto.sha256(BinaryData.empty), FailureMessageCodecs.BADONION) + sender.send(alice, fail) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments == initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail))) } - test("recv UpdateFailMalformedHtlc (invalid failure_code)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val fail = UpdateFailMalformedHtlc("00" * 32, 1, Crypto.sha256(BinaryData.empty), 42) - sender.send(alice, fail) - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data) === InvalidFailureCode("00" * 32).getMessage) - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - alice2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFailMalformedHtlc (invalid failure_code)") { f => + import f._ + val sender = TestProbe() + val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val fail = UpdateFailMalformedHtlc("00" * 32, 1, Crypto.sha256(BinaryData.empty), 42) + sender.send(alice, fail) + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data) === InvalidFailureCode("00" * 32).getMessage) + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv CMD_SIGN") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - // we need to have something to sign so we first send a fulfill and acknowledge (=sign) it - sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) - bob2alice.expectMsgType[UpdateFulfillHtlc] - bob2alice.forward(alice) - sender.send(bob, CMD_SIGN) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - alice2bob.forward(bob) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) - } + test("recv CMD_SIGN") { f => + import f._ + val sender = TestProbe() + // we need to have something to sign so we first send a fulfill and acknowledge (=sign) it + sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob2alice.forward(alice) + sender.send(bob, CMD_SIGN) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) } - test("recv CMD_SIGN (no changes)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, CMD_SIGN) - sender.expectNoMsg(1 second) // just ignored - //sender.expectMsg("cannot sign when there are no changes") - } + test("recv CMD_SIGN (no changes)") { f => + import f._ + val sender = TestProbe() + sender.send(alice, CMD_SIGN) + sender.expectNoMsg(1 second) // just ignored + //sender.expectMsg("cannot sign when there are no changes") } - test("recv CMD_SIGN (while waiting for RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) - sender.expectMsg("ok") - bob2alice.expectMsgType[UpdateFulfillHtlc] - sender.send(bob, CMD_SIGN) - sender.expectMsg("ok") - bob2alice.expectMsgType[CommitSig] - awaitCond(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) - val waitForRevocation = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) + test("recv CMD_SIGN (while waiting for RevokeAndAck)") { f => + import f._ + val sender = TestProbe() + sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateFulfillHtlc] + sender.send(bob, CMD_SIGN) + sender.expectMsg("ok") + bob2alice.expectMsgType[CommitSig] + awaitCond(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) + val waitForRevocation = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.left.toOption.get + assert(waitForRevocation.reSignAsap === false) - // actual test starts here - sender.send(bob, CMD_SIGN) - sender.expectNoMsg(300 millis) - assert(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo === Left(waitForRevocation)) - } + // actual test starts here + sender.send(bob, CMD_SIGN) + sender.expectNoMsg(300 millis) + assert(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo === Left(waitForRevocation)) } - test("recv CommitSig") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) - sender.expectMsg("ok") - bob2alice.expectMsgType[UpdateFulfillHtlc] - bob2alice.forward(alice) - sender.send(bob, CMD_SIGN) - sender.expectMsg("ok") - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - } + test("recv CommitSig") { f => + import f._ + val sender = TestProbe() + sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob2alice.forward(alice) + sender.send(bob, CMD_SIGN) + sender.expectMsg("ok") + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] } - test("recv CommitSig (no changes)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - // signature is invalid but it doesn't matter - sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) - bob2alice.expectMsgType[Error] - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx - bob2blockchain.expectMsgType[PublishAsap] // main delayed - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv CommitSig (no changes)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + // signature is invalid but it doesn't matter + sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + bob2blockchain.expectMsgType[PublishAsap] // main delayed + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv CommitSig (invalid signature)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) - bob2alice.expectMsgType[Error] - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx - bob2blockchain.expectMsgType[PublishAsap] // main delayed - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv CommitSig (invalid signature)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil)) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + bob2blockchain.expectMsgType[PublishAsap] // main delayed + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv RevokeAndAck (with remaining htlcs on both sides)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - fulfillHtlc(1, "22" * 32, bob, alice, bob2alice, alice2bob) - // this will cause alice and bob to receive RevokeAndAcks - crossSign(bob, alice, bob2alice, alice2bob) - // actual test starts here - assert(alice.stateName == SHUTDOWN) - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.spec.htlcs.size == 1) - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteCommit.spec.htlcs.size == 1) - } + test("recv RevokeAndAck (with remaining htlcs on both sides)") { f => + import f._ + fulfillHtlc(1, "22" * 32, bob, alice, bob2alice, alice2bob) + // this will cause alice and bob to receive RevokeAndAcks + crossSign(bob, alice, bob2alice, alice2bob) + // actual test starts here + assert(alice.stateName == SHUTDOWN) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.spec.htlcs.size == 1) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteCommit.spec.htlcs.size == 1) } - test("recv RevokeAndAck (with remaining htlcs on one side)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) - fulfillHtlc(1, "22" * 32, bob, alice, bob2alice, alice2bob) - val sender = TestProbe() - sender.send(bob, CMD_SIGN) - sender.expectMsg("ok") - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - // actual test starts here - bob2alice.forward(bob) - assert(alice.stateName == SHUTDOWN) - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.spec.htlcs.isEmpty) - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteCommit.spec.htlcs.size == 2) - } + test("recv RevokeAndAck (with remaining htlcs on one side)") { f => + import f._ + fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) + fulfillHtlc(1, "22" * 32, bob, alice, bob2alice, alice2bob) + val sender = TestProbe() + sender.send(bob, CMD_SIGN) + sender.expectMsg("ok") + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + // actual test starts here + bob2alice.forward(bob) + assert(alice.stateName == SHUTDOWN) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.spec.htlcs.isEmpty) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteCommit.spec.htlcs.size == 2) } - test("recv RevokeAndAck (no more htlcs on either side)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) - fulfillHtlc(1, "22" * 32, bob, alice, bob2alice, alice2bob) - crossSign(bob, alice, bob2alice, alice2bob) - // actual test starts here - awaitCond(alice.stateName == NEGOTIATING) - } + test("recv RevokeAndAck (no more htlcs on either side)") { f => + import f._ + fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) + fulfillHtlc(1, "22" * 32, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + // actual test starts here + awaitCond(alice.stateName == NEGOTIATING) } - test("recv RevokeAndAck (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) - sender.expectMsg("ok") - bob2alice.expectMsgType[UpdateFulfillHtlc] - bob2alice.forward(alice) - sender.send(bob, CMD_SIGN) - sender.expectMsg("ok") - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - awaitCond(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) - sender.send(bob, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) - bob2alice.expectMsgType[Error] - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx - bob2blockchain.expectMsgType[PublishAsap] // main delayed - bob2blockchain.expectMsgType[PublishAsap] // htlc success - bob2blockchain.expectMsgType[PublishAsap] // htlc delayed - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv RevokeAndAck (invalid preimage)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(bob, CMD_FULFILL_HTLC(0, "11" * 32)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob2alice.forward(alice) + sender.send(bob, CMD_SIGN) + sender.expectMsg("ok") + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + awaitCond(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) + sender.send(bob, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + bob2blockchain.expectMsgType[PublishAsap] // main delayed + bob2blockchain.expectMsgType[PublishAsap] // htlc success + bob2blockchain.expectMsgType[PublishAsap] // htlc delayed + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv RevokeAndAck (unexpectedly)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isRight) - sender.send(alice, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - alice2blockchain.expectMsgType[WatchConfirmed] - } + test("recv RevokeAndAck (unexpectedly)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isRight) + sender.send(alice, RevokeAndAck("00" * 32, Scalar("11" * 32), Scalar("22" * 32).toPoint)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv CMD_UPDATE_FEE") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(alice, CMD_UPDATE_FEE(20000)) - sender.expectMsg("ok") - val fee = alice2bob.expectMsgType[UpdateFee] - awaitCond(alice.stateData == initialState.copy( - commitments = initialState.commitments.copy( - localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee)))) - } + test("recv CMD_UPDATE_FEE") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(alice, CMD_UPDATE_FEE(20000)) + sender.expectMsg("ok") + val fee = alice2bob.expectMsgType[UpdateFee] + awaitCond(alice.stateData == initialState.copy( + commitments = initialState.commitments.copy( + localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee)))) } - test("recv CMD_UPDATE_FEE (when fundee)") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_UPDATE_FEE(20000)) - sender.expectMsg(Failure(FundeeCannotSendUpdateFee(channelId(bob)))) - assert(initialState == bob.stateData) - } + test("recv CMD_UPDATE_FEE (when fundee)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(bob, CMD_UPDATE_FEE(20000)) + sender.expectMsg(Failure(FundeeCannotSendUpdateFee(channelId(bob)))) + assert(initialState == bob.stateData) } - test("recv UpdateFee") { case (_, bob, _, _, _, _, _) => - within(30 seconds) { - val initialData = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val fee = UpdateFee("00" * 32, 12000) - bob ! fee - awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee)))) - } + test("recv UpdateFee") { f => + import f._ + val initialData = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + val fee = UpdateFee("00" * 32, 12000) + bob ! fee + awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee)))) } - test("recv UpdateFee (when sender is not funder)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(alice, UpdateFee("00" * 32, 12000)) - alice2bob.expectMsgType[Error] - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - alice2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFee (when sender is not funder)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(alice, UpdateFee("00" * 32, 12000)) + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 + alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateFee (sender can't afford it)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - val fee = UpdateFee("00" * 32, 100000000) - // we first update the global variable so that we don't trigger a 'fee too different' error - Globals.feeratesPerKw.set(FeeratesPerKw.single(fee.feeratePerKw)) - sender.send(bob, fee) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === CannotAffordFees(channelId(bob), missingSatoshis = 72120000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx - //bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees) - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFee (sender can't afford it)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + val fee = UpdateFee("00" * 32, 100000000) + // we first update the global variable so that we don't trigger a 'fee too different' error + Globals.feeratesPerKw.set(FeeratesPerKw.single(fee.feeratePerKw)) + sender.send(bob, fee) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === CannotAffordFees(channelId(bob), missingSatoshis = 72120000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + //bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees) + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateFee (local/remote feerates are too different)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(bob, UpdateFee("00" * 32, 65000)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx - bob2blockchain.expectMsgType[PublishAsap] // main delayed - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFee (local/remote feerates are too different)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(bob, UpdateFee("00" * 32, 65000)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + bob2blockchain.expectMsgType[PublishAsap] // main delayed + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv UpdateFee (remote feerate is too small)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - val sender = TestProbe() - sender.send(bob, UpdateFee("00" * 32, 252)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data) === "remote fee rate is too small: remoteFeeratePerKw=252") - awaitCond(bob.stateName == CLOSING) - bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx - bob2blockchain.expectMsgType[PublishAsap] // main delayed - bob2blockchain.expectMsgType[WatchConfirmed] - } + test("recv UpdateFee (remote feerate is too small)") { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + sender.send(bob, UpdateFee("00" * 32, 252)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data) === "remote fee rate is too small: remoteFeeratePerKw=252") + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + bob2blockchain.expectMsgType[PublishAsap] // main delayed + bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv CurrentBlockCount (no htlc timed out)") { case (alice, bob, alice2bob, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(alice, CurrentBlockCount(400143)) - awaitCond(alice.stateData == initialState) - } + test("recv CurrentBlockCount (no htlc timed out)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] + sender.send(alice, CurrentBlockCount(400143)) + awaitCond(alice.stateData == initialState) } - test("recv CurrentBlockCount (an htlc timed out)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx - sender.send(alice, CurrentBlockCount(400145)) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main delayed - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 - alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - val watch = alice2blockchain.expectMsgType[WatchConfirmed] - assert(watch.event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) - } + test("recv CurrentBlockCount (an htlc timed out)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] + val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx + sender.send(alice, CurrentBlockCount(400145)) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 + alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 + val watch = alice2blockchain.expectMsgType[WatchConfirmed] + assert(watch.event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) } - test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val event = CurrentFeerates(FeeratesPerKw.single(20000)) - sender.send(alice, event) - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) - } + test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] + val event = CurrentFeerates(FeeratesPerKw.single(20000)) + sender.send(alice, event) + alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2)) } - test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { case (alice, _, alice2bob, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val event = CurrentFeerates(FeeratesPerKw.single(10010)) - sender.send(alice, event) - alice2bob.expectNoMsg(500 millis) - } + test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => + import f._ + val sender = TestProbe() + val event = CurrentFeerates(FeeratesPerKw.single(10010)) + sender.send(alice, event) + alice2bob.expectNoMsg(500 millis) } - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are close)") { case (_, bob, _, bob2alice, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - val event = CurrentFeerates(FeeratesPerKw.single(11000)) - sender.send(bob, event) - bob2alice.expectNoMsg(500 millis) - } + test("recv CurrentFeerate (when fundee, commit-fee/network-fee are close)") { f => + import f._ + val sender = TestProbe() + val event = CurrentFeerates(FeeratesPerKw.single(11000)) + sender.send(bob, event) + bob2alice.expectNoMsg(500 millis) } - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) => - within(30 seconds) { - val sender = TestProbe() - val event = CurrentFeerates(FeeratesPerKw.single(1000)) - sender.send(bob, event) - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishAsap] // commit tx - bob2blockchain.expectMsgType[PublishAsap] // main delayed - bob2blockchain.expectMsgType[WatchConfirmed] - awaitCond(bob.stateName == CLOSING) - } + test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { f => + import f._ + val sender = TestProbe() + val event = CurrentFeerates(FeeratesPerKw.single(1000)) + sender.send(bob, event) + bob2alice.expectMsgType[Error] + bob2blockchain.expectMsgType[PublishAsap] // commit tx + bob2blockchain.expectMsgType[PublishAsap] // main delayed + bob2blockchain.expectMsgType[WatchConfirmed] + awaitCond(bob.stateName == CLOSING) } - test("recv BITCOIN_FUNDING_SPENT (their commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - // bob publishes his current commit tx, which contains two pending htlcs alice->bob - val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - assert(bobCommitTx.txOut.size == 4) // two main outputs and 2 pending htlcs - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + test("recv BITCOIN_FUNDING_SPENT (their commit)") { f => + import f._ + // bob publishes his current commit tx, which contains two pending htlcs alice->bob + val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + assert(bobCommitTx.txOut.size == 4) // two main outputs and 2 pending htlcs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - // in response to that, alice publishes its claim txes - val claimTxes = for (i <- 0 until 3) yield alice2blockchain.expectMsgType[PublishAsap].tx - // in addition to its main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { - assert(claimHtlcTx.txIn.size == 1) - assert(claimHtlcTx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut(0).amount - }).sum - // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat = 1000000 - 200000 = 800000 (because fees) - assert(amountClaimed == Satoshi(774010)) + // in response to that, alice publishes its claim txes + val claimTxes = for (i <- 0 until 3) yield alice2blockchain.expectMsgType[PublishAsap].tx + // in addition to its main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage + val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { + assert(claimHtlcTx.txIn.size == 1) + assert(claimHtlcTx.txOut.size == 1) + Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimHtlcTx.txOut(0).amount + }).sum + // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat = 1000000 - 200000 = 800000 (because fees) + assert(amountClaimed == Satoshi(774010)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - alice2blockchain.expectNoMsg(1 second) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + alice2blockchain.expectNoMsg(1 second) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 0) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) - } + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 0) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) } - test("recv BITCOIN_FUNDING_SPENT (their next commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - // bob fulfills the first htlc - fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) - // then signs - val sender = TestProbe() - sender.send(bob, CMD_SIGN) - sender.expectMsg("ok") - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - alice2bob.forward(bob) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] - // we intercept bob's revocation - // as far as alice knows, bob currently has two valid unrevoked commitment transactions + test("recv BITCOIN_FUNDING_SPENT (their next commit)") { f => + import f._ + // bob fulfills the first htlc + fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) + // then signs + val sender = TestProbe() + sender.send(bob, CMD_SIGN) + sender.expectMsg("ok") + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + // we intercept bob's revocation + // as far as alice knows, bob currently has two valid unrevoked commitment transactions - // bob publishes his current commit tx, which contains one pending htlc alice->bob - val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - assert(bobCommitTx.txOut.size == 3) // two main outputs and 1 pending htlc - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + // bob publishes his current commit tx, which contains one pending htlc alice->bob + val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + assert(bobCommitTx.txOut.size == 3) // two main outputs and 1 pending htlc + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - // in response to that, alice publishes its claim txes - val claimTxes = for (i <- 0 until 2) yield alice2blockchain.expectMsgType[PublishAsap].tx - // in addition to its main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { - assert(claimHtlcTx.txIn.size == 1) - assert(claimHtlcTx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut(0).amount - }).sum - // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat - htlc1 = 1000000 - 200000 - 300 000 = 500000 (because fees) - assert(amountClaimed == Satoshi(481190)) + // in response to that, alice publishes its claim txes + val claimTxes = for (i <- 0 until 2) yield alice2blockchain.expectMsgType[PublishAsap].tx + // in addition to its main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage + val amountClaimed = (for (claimHtlcTx <- claimTxes) yield { + assert(claimHtlcTx.txIn.size == 1) + assert(claimHtlcTx.txOut.size == 1) + Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimHtlcTx.txOut(0).amount + }).sum + // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat - htlc1 = 1000000 - 200000 - 300 000 = 500000 (because fees) + assert(amountClaimed == Satoshi(481190)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - alice2blockchain.expectNoMsg(1 second) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + alice2blockchain.expectNoMsg(1 second) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcSuccessTxs.size == 0) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcTimeoutTxs.size == 1) - } + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcSuccessTxs.size == 0) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimHtlcTimeoutTxs.size == 1) } - test("recv BITCOIN_FUNDING_SPENT (revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => - within(30 seconds) { - val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - // two main outputs + 2 htlc - assert(revokedTx.txOut.size == 4) + test("recv BITCOIN_FUNDING_SPENT (revoked tx)") { f => + import f._ + val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + // two main outputs + 2 htlc + assert(revokedTx.txOut.size == 4) - // bob fulfills one of the pending htlc (just so that he can have a new sig) - fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) - // bob and alice sign - crossSign(bob, alice, bob2alice, alice2bob) - // bob now has a new commitment tx + // bob fulfills one of the pending htlc (just so that he can have a new sig) + fulfillHtlc(0, "11" * 32, bob, alice, bob2alice, alice2bob) + // bob and alice sign + crossSign(bob, alice, bob2alice, alice2bob) + // bob now has a new commitment tx - // bob published the revoked tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx) - alice2bob.expectMsgType[Error] + // bob published the revoked tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx) + alice2bob.expectMsgType[Error] - val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx - val htlc1PenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx - val htlc2PenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx - assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx)) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // htlc1-penalty - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // htlc2-penalty - alice2blockchain.expectNoMsg(1 second) + val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + val htlc1PenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + val htlc2PenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx)) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // htlc1-penalty + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // htlc2-penalty + alice2blockchain.expectNoMsg(1 second) - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlc1PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlc2PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(htlc1PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(htlc2PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 - assert(mainTx.txOut(0).amount == Satoshi(284930)) - assert(mainPenaltyTx.txOut(0).amount == Satoshi(195150)) - assert(htlc1PenaltyTx.txOut(0).amount == Satoshi(194530)) - assert(htlc2PenaltyTx.txOut(0).amount == Satoshi(294530)) + // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 + assert(mainTx.txOut(0).amount == Satoshi(284930)) + assert(mainPenaltyTx.txOut(0).amount == Satoshi(195150)) + assert(htlc1PenaltyTx.txOut(0).amount == Satoshi(194530)) + assert(htlc2PenaltyTx.txOut(0).amount == Satoshi(294530)) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - } + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) } - test("recv CMD_CLOSE") { case (alice, _, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) - } + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) } - test("recv Error") { case (alice, _, _, _, alice2blockchain, _, _) => - within(30 seconds) { - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx - alice ! Error("00" * 32, "oops".getBytes) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) - assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs + test("recv Error") { f => + import f._ + val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx + alice ! Error("00" * 32, "oops".getBytes) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) + assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs - // alice can claim both htlc after a timeout - // so we expect 5 transactions: - // - 1 tx to claim the main delayed output - // - 2 txes for each htlc - // - 2 txes for each delayed output of the claimed htlc - val claimTxs = for (i <- 0 until 5) yield alice2blockchain.expectMsgType[PublishAsap].tx + // alice can claim both htlc after a timeout + // so we expect 5 transactions: + // - 1 tx to claim the main delayed output + // - 2 txes for each htlc + // - 2 txes for each delayed output of the claimed htlc + val claimTxs = for (i <- 0 until 5) yield alice2blockchain.expectMsgType[PublishAsap].tx - // the main delayed output spends the commitment transaction - Transaction.correctlySpends(claimTxs(0), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // the main delayed output spends the commitment transaction + Transaction.correctlySpends(claimTxs(0), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - // 2nd stage transactions spend the commitment transaction - Transaction.correctlySpends(claimTxs(1), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(claimTxs(2), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // 2nd stage transactions spend the commitment transaction + Transaction.correctlySpends(claimTxs(1), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimTxs(2), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - // 3rd stage transactions spend their respective HTLC-Timeout transactions - Transaction.correctlySpends(claimTxs(3), claimTxs(1) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(claimTxs(4), claimTxs(2) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // 3rd stage transactions spend their respective HTLC-Timeout transactions + Transaction.correctlySpends(claimTxs(3), claimTxs(1) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimTxs(4), claimTxs(2) :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(0))) // main-delayed - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(3))) // htlc-delayed - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(4))) // htlc-delayed - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - alice2blockchain.expectNoMsg(1 second) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(0))) // main-delayed + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(3))) // htlc-delayed + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxs(4))) // htlc-delayed + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + alice2blockchain.expectNoMsg(1 second) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) - } + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) } } 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 82bfd400d..7de370327 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 @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel.states.g import akka.actor.Status.Failure +import akka.event.LoggingAdapter import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.TestConstants.Bob @@ -26,11 +27,9 @@ import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment.Local -import fr.acinq.eclair.wire.{ClosingSigned, CommitSig, Error, Shutdown} +import fr.acinq.eclair.wire.{ClosingSigned, Error, Shutdown} import fr.acinq.eclair.{Globals, TestkitBaseClass} -import org.junit.runner.RunWith -import org.scalatest.Tag -import org.scalatest.junit.JUnitRunner +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ import scala.util.Success @@ -38,12 +37,12 @@ import scala.util.Success /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ within(30 seconds) { @@ -62,161 +61,147 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4316)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(5000)) alice2bob.forward(bob) awaitCond(bob.stateName == NEGOTIATING) - test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain))) } } - test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _) => - within(30 seconds) { - alice2bob.expectMsgType[ClosingSigned] - val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) - sender.send(alice, add) - val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + test("recv CMD_ADD_HTLC") { f => + import f._ + alice2bob.expectMsgType[ClosingSigned] + val sender = TestProbe() + val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) + sender.send(alice, add) + val error = ChannelUnavailable(channelId(alice)) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { case (alice, bob, alice2bob, bob2alice, _, _) => - within(30 seconds) { - // alice initiates the negotiation - val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] + test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { f => + import f._ + // alice initiates the negotiation + val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] + alice2bob.forward(bob) + // bob answers with a counter proposition + val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] + assert(aliceCloseSig1.feeSatoshis > bobCloseSig1.feeSatoshis) + // actual test starts here + val initialState = alice.stateData.asInstanceOf[DATA_NEGOTIATING] + bob2alice.forward(alice) + val aliceCloseSig2 = alice2bob.expectMsgType[ClosingSigned] + // BOLT 2: If the receiver [doesn't agree with the fee] it SHOULD propose a value strictly between the received fee-satoshis and its previously-sent fee-satoshis + assert(aliceCloseSig2.feeSatoshis < aliceCloseSig1.feeSatoshis && aliceCloseSig2.feeSatoshis > bobCloseSig1.feeSatoshis) + awaitCond(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.map(_.localClosingSigned) == initialState.closingTxProposed.last.map(_.localClosingSigned) :+ aliceCloseSig2) + } + + private def testFeeConverge(f: FixtureParam) = { + import f._ + var aliceCloseFee, bobCloseFee = 0L + do { + aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) - // bob answers with a counter proposition - val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis > bobCloseSig1.feeSatoshis) - // actual test starts here - val initialState = alice.stateData.asInstanceOf[DATA_NEGOTIATING] - bob2alice.forward(alice) - val aliceCloseSig2 = alice2bob.expectMsgType[ClosingSigned] - // BOLT 2: If the receiver [doesn't agree with the fee] it SHOULD propose a value strictly between the received fee-satoshis and its previously-sent fee-satoshis - assert(aliceCloseSig2.feeSatoshis < aliceCloseSig1.feeSatoshis && aliceCloseSig2.feeSatoshis > bobCloseSig1.feeSatoshis) - awaitCond(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.map(_.localClosingSigned) == initialState.closingTxProposed.last.map(_.localClosingSigned) :+ aliceCloseSig2) - } + if (!bob2blockchain.msgAvailable) { + bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis + bob2alice.forward(alice) + } + } while (!alice2blockchain.msgAvailable && !bob2blockchain.msgAvailable) } - def testFeeConverge(alice: TestFSMRef[State, Data, Channel], - bob: TestFSMRef[State, Data, Channel], - alice2bob: TestProbe, - bob2alice: TestProbe, - alice2blockchain: TestProbe, - bob2blockchain: TestProbe) = { - within(30 seconds) { - var aliceCloseFee, bobCloseFee = 0L - do { - aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis - alice2bob.forward(bob) - if (!bob2blockchain.msgAvailable) { - bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis + test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 1)") { f => + testFeeConverge(f) + } + + test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 2)", Tag("fee2")) { f => + testFeeConverge(f) + } + + test("recv ClosingSigned (fee too high)") { f => + import f._ + 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 + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data).startsWith("invalid close fee: fee_satoshis=99000")) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv ClosingSigned (invalid sig)") { f => + import f._ + 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(signature = "00" * 64)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data).startsWith("invalid close signature")) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv BITCOIN_FUNDING_SPENT (counterparty's mutual close)") { f => + import f._ + var aliceCloseFee, bobCloseFee = 0L + do { + aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis + alice2bob.forward(bob) + if (!bob2blockchain.msgAvailable) { + bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis + if (aliceCloseFee != bobCloseFee) { bob2alice.forward(alice) } - } while (!alice2blockchain.msgAvailable && !bob2blockchain.msgAvailable) - } + } + } while (!alice2blockchain.msgAvailable && !bob2blockchain.msgAvailable) + // at this point alice and bob have converged on closing fees, but alice has not yet received the final signature whereas bob has + // bob publishes the mutual close and alice is notified that the funding tx has been spent + // actual test starts here + assert(alice.stateName == NEGOTIATING) + val mutualCloseTx = bob2blockchain.expectMsgType[PublishAsap].tx + assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTx)) + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + alice2blockchain.expectNoMsg(100 millis) + assert(alice.stateName == CLOSING) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 1)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) => - testFeeConverge(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - } + test("recv BITCOIN_FUNDING_SPENT (an older mutual close)") { f => + import f._ + val aliceClose1 = alice2bob.expectMsgType[ClosingSigned] + alice2bob.forward(bob) + val bobClose1 = bob2alice.expectMsgType[ClosingSigned] + bob2alice.forward(alice) + val aliceClose2 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceClose2.feeSatoshis != bobClose1.feeSatoshis) + // 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) - test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 2)", Tag("fee2")) { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) => - testFeeConverge(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - } - - test("recv ClosingSigned (fee too high)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) => - within(30 seconds) { - 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 - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data).startsWith("invalid close fee: fee_satoshis=99000")) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv ClosingSigned (invalid sig)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) => - within(30 seconds) { - 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(signature = "00" * 64)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data).startsWith("invalid close signature")) - bob2blockchain.expectMsg(PublishAsap(tx)) - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[WatchConfirmed] - } - } - - test("recv BITCOIN_FUNDING_SPENT (counterparty's mutual close)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) => - within(30 seconds) { - var aliceCloseFee, bobCloseFee = 0L - do { - aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis - alice2bob.forward(bob) - if (!bob2blockchain.msgAvailable) { - bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis - if (aliceCloseFee != bobCloseFee) { - bob2alice.forward(alice) - } - } - } while (!alice2blockchain.msgAvailable && !bob2blockchain.msgAvailable) - // at this point alice and bob have converged on closing fees, but alice has not yet received the final signature whereas bob has - // bob publishes the mutual close and alice is notified that the funding tx has been spent - // actual test starts here - assert(alice.stateName == NEGOTIATING) - val mutualCloseTx = bob2blockchain.expectMsgType[PublishAsap].tx - assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTx)) - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - alice2blockchain.expectNoMsg(100 millis) - assert(alice.stateName == CLOSING) - } - } - - test("recv BITCOIN_FUNDING_SPENT (an older mutual close)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) => - within(30 seconds) { - val aliceClose1 = alice2bob.expectMsgType[ClosingSigned] - alice2bob.forward(bob) - val bobClose1 = bob2alice.expectMsgType[ClosingSigned] - bob2alice.forward(alice) - val aliceClose2 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceClose2.feeSatoshis != bobClose1.feeSatoshis) - // 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 = bob.underlyingActor.implicitLog - val Success(bobClosingTx) = Closing.checkClosingSignature(Bob.keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(aliceClose1.feeSatoshis), aliceClose1.signature) - - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed] - alice2blockchain.expectNoMsg(100 millis) - assert(alice.stateName == CLOSING) - } + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed] + alice2blockchain.expectNoMsg(100 millis) + assert(alice.stateName == CLOSING) } - test("recv CMD_CLOSE") { case (alice, _, _, _, _, _) => - within(30 seconds) { - val sender = TestProbe() - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) - } + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) } - test("recv Error") { case (alice, _, _, _, alice2blockchain, _) => - within(30 seconds) { - val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx - alice ! Error("00" * 32, "oops".getBytes()) - awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsg(PublishAsap(tx)) - alice2blockchain.expectMsgType[PublishAsap] - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) - } + test("recv Error") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx + alice ! Error("00" * 32, "oops".getBytes()) + awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(tx)) + alice2blockchain.expectMsgType[PublishAsap] + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) } } 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 ec9f4274b..dcb840237 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 @@ -20,7 +20,6 @@ import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{OutPoint, ScriptFlags, Transaction, TxIn} -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel.states.StateTestsHelperMethods @@ -28,24 +27,24 @@ import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment.{CommandBuffer, ForwardAdd, ForwardFulfill, Local} import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 05/07/2016. */ -@RunWith(classOf[JUnitRunner]) + class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - type FixtureParam = Tuple8[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe, List[PublishableTxs]] + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe, bobCommitTxes: List[PublishableTxs]) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val bobCommitTxes = within(30 seconds) { + within(30 seconds) { reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer) val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000, 200000000, 300000000)) yield { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) @@ -71,9 +70,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // - revoked commit // and we want to be able to test the different scenarii. // Hence the NORMAL->CLOSING transition will occur in the individual tests. - bobCommitTxes + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer, bobCommitTxes))) } - test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer, bobCommitTxes)) } def mutualClose(alice: TestFSMRef[State, Data, Channel], @@ -106,438 +104,420 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // both nodes are now in CLOSING state with a mutual close tx pending for confirmation } - test("recv CMD_ADD_HTLC") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + test("recv CMD_ADD_HTLC") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // actual test starts here - val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) - sender.send(alice, add) - val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) - alice2bob.expectNoMsg(200 millis) - } + // actual test starts here + val sender = TestProbe() + val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) + sender.send(alice, add) + val error = ChannelUnavailable(channelId(alice)) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + alice2bob.expectNoMsg(200 millis) } - test("recv CMD_FULFILL_HTLC (unexisting htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + test("recv CMD_FULFILL_HTLC (unexisting htlc)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // actual test starts here - val sender = TestProbe() - sender.send(alice, CMD_FULFILL_HTLC(42, "42" * 32)) - sender.expectMsg(Failure(UnknownHtlcId(channelId(alice), 42))) + // actual test starts here + val sender = TestProbe() + sender.send(alice, CMD_FULFILL_HTLC(42, "42" * 32)) + sender.expectMsg(Failure(UnknownHtlcId(channelId(alice), 42))) - // NB: nominal case is tested in IntegrationSpec - } + // NB: nominal case is tested in IntegrationSpec } - test("recv BITCOIN_FUNDING_SPENT (mutual close before converging)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => - within(30 seconds) { - val sender = TestProbe() - // alice initiates a closing - sender.send(alice, CMD_CLOSE(None)) - alice2bob.expectMsgType[Shutdown] - alice2bob.forward(bob) - bob2alice.expectMsgType[Shutdown] - bob2alice.forward(alice) - // agreeing on a closing fee - val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis - Globals.feeratesPerKw.set(FeeratesPerKw.single(100)) - alice2bob.forward(bob) - val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis - bob2alice.forward(alice) - // they don't converge yet, but alice has a publishable commit tx now - assert(aliceCloseFee != bobCloseFee) - val Some(mutualCloseTx) = alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt - // let's make alice publish this closing tx - alice ! Error("00" * 32, "") - awaitCond(alice.stateName == CLOSING) - assert(mutualCloseTx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) + test("recv BITCOIN_FUNDING_SPENT (mutual close before converging)") { f => + import f._ + val sender = TestProbe() + // alice initiates a closing + sender.send(alice, CMD_CLOSE(None)) + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + // agreeing on a closing fee + val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis + Globals.feeratesPerKw.set(FeeratesPerKw.single(100)) + alice2bob.forward(bob) + val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis + bob2alice.forward(alice) + // they don't converge yet, but alice has a publishable commit tx now + assert(aliceCloseFee != bobCloseFee) + val Some(mutualCloseTx) = alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt + // let's make alice publish this closing tx + alice ! Error("00" * 32, "") + awaitCond(alice.stateName == CLOSING) + assert(mutualCloseTx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) - // actual test starts here - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx), 0, 0) - awaitCond(alice.stateName == CLOSED) - } + // actual test starts here + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx), 0, 0) + awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (mutual close)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val mutualCloseTx = alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last + test("recv BITCOIN_TX_CONFIRMED (mutual close)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val mutualCloseTx = alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last - // actual test starts here - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx), 0, 0) - awaitCond(alice.stateName == CLOSED) - } + // actual test starts here + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx), 0, 0) + awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_FUNDING_SPENT (local commit)") { case (alice, _, _, _, alice2blockchain, _, _, _) => - within(30 seconds) { - // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice ! Error("00" * 32, "oops".getBytes) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid - awaitCond(alice.stateName == CLOSING) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.localCommitPublished.isDefined) + test("recv BITCOIN_FUNDING_SPENT (local commit)") { f => + import f._ + // an error occurs and alice publishes her commit tx + val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + alice ! Error("00" * 32, "oops".getBytes) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid + awaitCond(alice.stateName == CLOSING) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(initialState.localCommitPublished.isDefined) - // actual test starts here - // we are notified afterwards from our watcher about the tx that we just published - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, aliceCommitTx) - assert(alice.stateData == initialState) // this was a no-op - } + // actual test starts here + // we are notified afterwards from our watcher about the tx that we just published + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, aliceCommitTx) + assert(alice.stateData == initialState) // this was a no-op } - test("recv BITCOIN_OUTPUT_SPENT") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer, _) => - within(30 seconds) { - // alice sends an htlc to bob - val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - relayer.expectMsgType[ForwardAdd] - // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice ! Error("00" * 32, "oops".getBytes) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx - alice2blockchain.expectMsgType[PublishAsap] // main-delayed-output - alice2blockchain.expectMsgType[PublishAsap] // htlc-timeout - alice2blockchain.expectMsgType[PublishAsap] // claim-delayed-output - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // main-delayed-output - assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // claim-delayed-output - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - awaitCond(alice.stateName == CLOSING) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.localCommitPublished.isDefined) + test("recv BITCOIN_OUTPUT_SPENT") { f => + import f._ + // alice sends an htlc to bob + val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + relayer.expectMsgType[ForwardAdd] + // an error occurs and alice publishes her commit tx + val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + alice ! Error("00" * 32, "oops".getBytes) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx + alice2blockchain.expectMsgType[PublishAsap] // main-delayed-output + alice2blockchain.expectMsgType[PublishAsap] // htlc-timeout + alice2blockchain.expectMsgType[PublishAsap] // claim-delayed-output + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // main-delayed-output + assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // claim-delayed-output + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + awaitCond(alice.stateName == CLOSING) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(initialState.localCommitPublished.isDefined) - // actual test starts here - relayer.expectMsgType[LocalChannelDown] + // actual test starts here + relayer.expectMsgType[LocalChannelDown] - // scenario 1: bob claims the htlc output from the commit tx using its preimage - val claimHtlcSuccessFromCommitTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx("11" * 70, ra1, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0) - alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, claimHtlcSuccessFromCommitTx) - assert(relayer.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1)) + // scenario 1: bob claims the htlc output from the commit tx using its preimage + val claimHtlcSuccessFromCommitTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx("11" * 70, ra1, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0) + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, claimHtlcSuccessFromCommitTx) + assert(relayer.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1)) - // scenario 2: bob claims the htlc output from his own commit tx using its preimage (let's assume both parties had published their commitment tx) - val claimHtlcSuccessTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessHtlcSuccess("11" * 70, "22" * 70, ra1, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0) - alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, claimHtlcSuccessTx) - assert(relayer.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1)) + // scenario 2: bob claims the htlc output from his own commit tx using its preimage (let's assume both parties had published their commitment tx) + val claimHtlcSuccessTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessHtlcSuccess("11" * 70, "22" * 70, ra1, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0) + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, claimHtlcSuccessTx) + assert(relayer.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1)) - assert(alice.stateData == initialState) // this was a no-op - } + assert(alice.stateData == initialState) // this was a no-op } - test("recv BITCOIN_TX_CONFIRMED (local commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _, _) => - within(30 seconds) { - val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) - // alice sends an htlc to bob - val (ra1, htlca1) = addHtlc(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 - alice ! Error("00" * 32, "oops".getBytes) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx - val claimMainDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-delayed-output - val htlcTimeoutTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-timeout - val claimDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-delayed-output - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // main-delayed-output - assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // claim-delayed-output - assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) - awaitCond(alice.stateName == CLOSING) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.localCommitPublished.isDefined) + test("recv BITCOIN_TX_CONFIRMED (local commit)") { f => + import f._ + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) + // alice sends an htlc to bob + val (ra1, htlca1) = addHtlc(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 + alice ! Error("00" * 32, "oops".getBytes) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx + val claimMainDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-delayed-output + val htlcTimeoutTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-timeout + val claimDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-delayed-output + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // main-delayed-output + assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // claim-delayed-output + assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) + awaitCond(alice.stateName == CLOSING) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(initialState.localCommitPublished.isDefined) - // actual test starts here - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 42, 0) - assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == 42 + TestConstants.Bob.channelParams.toSelfDelay) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainDelayedTx), 200, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcTimeoutTx), 201, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimDelayedTx), 202, 0) - awaitCond(alice.stateName == CLOSED) - } + // actual test starts here + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 42, 0) + assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == 42 + TestConstants.Bob.channelParams.toSelfDelay) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainDelayedTx), 200, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcTimeoutTx), 201, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimDelayedTx), 202, 0) + awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (local commit with htlcs only signed by local)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer, _) => - within(30 seconds) { - val sender = TestProbe() - 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) - // and signs it (but bob doesn't sign it) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - // note that bob doesn't receive the new sig! - // then we make alice unilaterally close the channel - alice ! Error("00" * 32, "oops".getBytes) - alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) - awaitCond(alice.stateName == CLOSING) - val aliceData = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceData.localCommitPublished.isDefined) - relayer.expectMsgType[LocalChannelDown] + test("recv BITCOIN_TX_CONFIRMED (local commit with htlcs only signed by local)") { f => + import f._ + val sender = TestProbe() + 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) + // and signs it (but bob doesn't sign it) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + // note that bob doesn't receive the new sig! + // then we make alice unilaterally close the channel + alice ! Error("00" * 32, "oops".getBytes) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) + awaitCond(alice.stateName == CLOSING) + val aliceData = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(aliceData.localCommitPublished.isDefined) + relayer.expectMsgType[LocalChannelDown] - // actual test starts here - // when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 0, 0) - // so she fails it - val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayer.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None))) - } + // actual test starts here + // when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 0, 0) + // so she fails it + val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) + relayer.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None))) } - test("recv BITCOIN_TX_CONFIRMED (remote commit with htlcs only signed by local in next remote commit)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, relayer, _) => - within(30 seconds) { - val sender = TestProbe() - 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) - // and signs it (but bob doesn't sign it) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - // then we make alice believe bob unilaterally close the channel - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - awaitCond(alice.stateName == CLOSING) - val aliceData = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceData.remoteCommitPublished.isDefined) - relayer.expectMsgType[LocalChannelDown] + test("recv BITCOIN_TX_CONFIRMED (remote commit with htlcs only signed by local in next remote commit)") { f => + import f._ + val sender = TestProbe() + 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) + // and signs it (but bob doesn't sign it) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + // then we make alice believe bob unilaterally close the channel + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + awaitCond(alice.stateName == CLOSING) + val aliceData = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(aliceData.remoteCommitPublished.isDefined) + relayer.expectMsgType[LocalChannelDown] - // actual test starts here - // when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0) - // so she fails it - val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayer.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None))) - } + // actual test starts here + // when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0) + // so she fails it + val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) + relayer.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None))) } - test("recv BITCOIN_FUNDING_SPENT (remote commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxes.last.commitTx.tx - assert(bobCommitTx.txOut.size == 2) // two main outputs - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + test("recv BITCOIN_FUNDING_SPENT (remote commit)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state + val bobCommitTx = bobCommitTxes.last.commitTx.tx + assert(bobCommitTx.txOut.size == 2) // two main outputs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - alice2blockchain.expectMsgType[PublishAsap] - alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid + alice2blockchain.expectMsgType[PublishAsap] + alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) - } + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) } - test("recv BITCOIN_TX_CONFIRMED (remote commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxes.last.commitTx.tx - assert(bobCommitTx.txOut.size == 2) // two main outputs - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx - alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) + test("recv BITCOIN_TX_CONFIRMED (remote commit)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state + val bobCommitTx = bobCommitTxes.last.commitTx.tx + assert(bobCommitTx.txOut.size == 2) // two main outputs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx + alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) - // actual test starts here - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) - awaitCond(alice.stateName == CLOSED) - } + // actual test starts here + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) + awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) => - within(30 seconds) { - val sender = TestProbe() - val oldStateData = alice.stateData - val (ra1, htlca1) = addHtlc(25000000, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) - crossSign(bob, alice, bob2alice, alice2bob) - // we simulate a disconnection - sender.send(alice, INPUT_DISCONNECTED) - sender.send(bob, INPUT_DISCONNECTED) - awaitCond(alice.stateName == OFFLINE) - awaitCond(bob.stateName == OFFLINE) - // then we manually replace alice's state with an older one - alice.setState(OFFLINE, oldStateData) - // then we reconnect them - sender.send(alice, INPUT_RECONNECTED(alice2bob.ref)) - sender.send(bob, INPUT_RECONNECTED(bob2alice.ref)) - // peers exchange channel_reestablish messages - alice2bob.expectMsgType[ChannelReestablish] - bob2alice.expectMsgType[ChannelReestablish] - // alice then realizes it has an old state... - bob2alice.forward(alice) - // ... and ask bob to publish its current commitment - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data) === PleasePublishYourCommitment(channelId(alice)).getMessage) - // alice now waits for bob to publish its commitment - awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) - // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - // alice is able to claim its main output - val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx - Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) + test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f => + import f._ + val sender = TestProbe() + val oldStateData = alice.stateData + val (ra1, htlca1) = addHtlc(25000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + // then we manually replace alice's state with an older one + alice.setState(OFFLINE, oldStateData) + // then we reconnect them + sender.send(alice, INPUT_RECONNECTED(alice2bob.ref)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref)) + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + bob2alice.expectMsgType[ChannelReestablish] + // alice then realizes it has an old state... + bob2alice.forward(alice) + // ... and ask bob to publish its current commitment + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data) === PleasePublishYourCommitment(channelId(alice)).getMessage) + // alice now waits for bob to publish its commitment + awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) + // bob is nice and publishes its commitment + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + // alice is able to claim its main output + val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx + Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) - // actual test starts here - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) - awaitCond(alice.stateName == CLOSED) - } + // actual test starts here + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) + awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - // bob publishes one of his revoked txes - val bobRevokedTx = bobCommitTxes.head.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) + test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + // bob publishes one of his revoked txes + val bobRevokedTx = bobCommitTxes.head.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) - // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty - alice2blockchain.expectNoMsg(1 second) + // alice publishes and watches the penalty tx + alice2blockchain.expectMsgType[PublishAsap] // claim-main + alice2blockchain.expectMsgType[PublishAsap] // main-penalty + alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty + alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit + alice2blockchain.expectMsgType[WatchConfirmed] // claim-main + alice2blockchain.expectMsgType[WatchSpent] // main-penalty + alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + alice2blockchain.expectNoMsg(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) - } + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) } - test("recv BITCOIN_FUNDING_SPENT (multiple revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // bob publishes multiple revoked txes (last one isn't revoked) - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(0).commitTx.tx) - // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty - alice2blockchain.expectNoMsg(1 second) + test("recv BITCOIN_FUNDING_SPENT (multiple revoked tx)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + // bob publishes multiple revoked txes (last one isn't revoked) + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(0).commitTx.tx) + // alice publishes and watches the penalty tx + alice2blockchain.expectMsgType[PublishAsap] // claim-main + alice2blockchain.expectMsgType[PublishAsap] // main-penalty + alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty + alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit + alice2blockchain.expectMsgType[WatchConfirmed] // claim-main + alice2blockchain.expectMsgType[WatchSpent] // main-penalty + alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + alice2blockchain.expectNoMsg(1 second) - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(1).commitTx.tx) - // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectNoMsg(1 second) + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(1).commitTx.tx) + // alice publishes and watches the penalty tx + alice2blockchain.expectMsgType[PublishAsap] // claim-main + alice2blockchain.expectMsgType[PublishAsap] // main-penalty + alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit + alice2blockchain.expectMsgType[WatchConfirmed] // claim-main + alice2blockchain.expectMsgType[WatchSpent] // main-penalty + alice2blockchain.expectNoMsg(1 second) - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(2).commitTx.tx) - // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty - alice2blockchain.expectNoMsg(1 second) + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(2).commitTx.tx) + // alice publishes and watches the penalty tx + alice2blockchain.expectMsgType[PublishAsap] // claim-main + alice2blockchain.expectMsgType[PublishAsap] // main-penalty + alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty + alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit + alice2blockchain.expectMsgType[WatchConfirmed] // claim-main + alice2blockchain.expectMsgType[WatchSpent] // main-penalty + alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + alice2blockchain.expectNoMsg(1 second) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 3) - } + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 3) } - test("recv BITCOIN_OUTPUT_SPENT (one revoked tx, counterparty published HtlcSuccess tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // bob publishes one of his revoked txes - val bobRevokedTx = bobCommitTxes.head - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) - // alice publishes and watches the penalty tx - val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty - alice2blockchain.expectNoMsg(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx) + test("recv BITCOIN_OUTPUT_SPENT (one revoked tx, counterparty published HtlcSuccess tx)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + // bob publishes one of his revoked txes + val bobRevokedTx = bobCommitTxes.head + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) + // alice publishes and watches the penalty tx + val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty + val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty + alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit + alice2blockchain.expectMsgType[WatchConfirmed] // claim-main + alice2blockchain.expectMsgType[WatchSpent] // main-penalty + alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + alice2blockchain.expectNoMsg(1 second) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx) - // actual test starts here - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx.commitTx.tx), 0, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0) - alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx) // we published this - alice2blockchain.expectMsgType[WatchConfirmed] // htlc-penalty - val bobHtlcSuccessTx = bobRevokedTx.htlcTxsAndSigs.head.txinfo.tx - alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx) // bob published his HtlcSuccess tx - alice2blockchain.expectMsgType[WatchConfirmed] // htlc-success - val claimHtlcDelayedPenaltyTxs = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's HtlcSuccess tx - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcSuccessTx), 0, 0) // bob won - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcDelayedPenaltyTxs), 0, 0) // bob won - awaitCond(alice.stateName == CLOSED) - } + // actual test starts here + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx.commitTx.tx), 0, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0) + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx) // we published this + alice2blockchain.expectMsgType[WatchConfirmed] // htlc-penalty + val bobHtlcSuccessTx = bobRevokedTx.htlcTxsAndSigs.head.txinfo.tx + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx) // bob published his HtlcSuccess tx + alice2blockchain.expectMsgType[WatchConfirmed] // htlc-success + val claimHtlcDelayedPenaltyTxs = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's HtlcSuccess tx + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcSuccessTx), 0, 0) // bob won + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcDelayedPenaltyTxs), 0, 0) // bob won + awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (one revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // bob publishes one of his revoked txes - val bobRevokedTx = bobCommitTxes.head - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) - // alice publishes and watches the penalty tx - val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty - alice2blockchain.expectNoMsg(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx) + test("recv BITCOIN_TX_CONFIRMED (one revoked tx)") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + // bob publishes one of his revoked txes + val bobRevokedTx = bobCommitTxes.head + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) + // alice publishes and watches the penalty tx + val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty + val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty + alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit + alice2blockchain.expectMsgType[WatchConfirmed] // claim-main + alice2blockchain.expectMsgType[WatchSpent] // main-penalty + alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + alice2blockchain.expectNoMsg(1 second) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx) - // actual test starts here - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx.commitTx.tx), 0, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0) - alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcPenaltyTx), 0, 0) - awaitCond(alice.stateName == CLOSED) - } + // actual test starts here + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx.commitTx.tx), 0, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0) + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcPenaltyTx), 0, 0) + awaitCond(alice.stateName == CLOSED) } - test("recv ChannelReestablish") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - val sender = TestProbe() - sender.send(alice, ChannelReestablish(channelId(bob), 42, 42)) - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data) === FundingTxSpent(channelId(alice), initialState.spendingTxes.head).getMessage) - } + test("recv ChannelReestablish") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + val sender = TestProbe() + sender.send(alice, ChannelReestablish(channelId(bob), 42, 42)) + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data) === FundingTxSpent(channelId(alice), initialState.spendingTxes.head).getMessage) } - test("recv CMD_CLOSE") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => - within(30 seconds) { - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val sender = TestProbe() - sender.send(alice, CMD_CLOSE(None)) - sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) - } + test("recv CMD_CLOSE") { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(ClosingAlreadyInProgress(channelId(alice)))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/BitStreamSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/BitStreamSpec.scala index 645397122..403643bba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/BitStreamSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/BitStreamSpec.scala @@ -16,15 +16,13 @@ package fr.acinq.eclair.crypto -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import org.spongycastle.util.encoders.Hex /** * Created by fabrice on 22/06/17. */ -@RunWith(classOf[JUnitRunner]) + class BitStreamSpec extends FunSuite { import BitStream._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala index cd4a8a2bc..151a8439c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala @@ -17,12 +17,10 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.BinaryData -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class ChaCha20Poly1305Spec extends FunSuite { test("Poly1305") { // from https://tools.ietf.org/html/rfc7539 ch 2.5.2 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala index 43fc0ed63..9fca57a80 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala @@ -18,11 +18,9 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.{Point, Scalar} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class GeneratorsSpec extends FunSuite { val base_secret: Scalar = BinaryData("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") val per_commitment_secret: Scalar = BinaryData("0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala index cb7078161..1fcde60be 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala @@ -19,11 +19,9 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.DeterministicWallet.KeyPath import fr.acinq.bitcoin.{BinaryData, Block} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class LocalKeyManagerSpec extends FunSuite { test("generate the same node id from the same seed") { // if this test breaks it means that we will generate a different node id from diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala index 15e68a5e8..d68862417 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala @@ -18,12 +18,10 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.crypto.Noise._ -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import org.spongycastle.crypto.ec.CustomNamedCurves -@RunWith(classOf[JUnitRunner]) + class NoiseSpec extends FunSuite { import Noise._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala index e292a23b8..a9f7c83aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala @@ -17,11 +17,9 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.{BinaryData, Hash} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class ShaChainSpec extends FunSuite { val expected: List[BinaryData] = List( "f85a0f7f237ed2139855703db4264de380ec731f64a3d832c22a5f2ef615f1d5", diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index cf8c56b80..b5c21c40c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -19,17 +19,14 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.wire._ -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -import scodec.bits.BitVector import scala.util.Success /** * Created by fabrice on 10/01/17. */ -@RunWith(classOf[JUnitRunner]) + class SphinxSpec extends FunSuite { import Sphinx._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala index 3541876f1..c951565c2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala @@ -25,8 +25,6 @@ import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.crypto.Noise.{Chacha20Poly1305CipherFunctions, CipherState} import fr.acinq.eclair.crypto.TransportHandler.{Encryptor, ExtendedCipherState, Listener} import fr.acinq.eclair.wire.LightningMessageCodecs -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scodec.Codec import scodec.codecs._ @@ -34,7 +32,7 @@ import scodec.codecs._ import scala.annotation.tailrec import scala.concurrent.duration._ -@RunWith(classOf[JUnitRunner]) + class TransportHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll { import TransportHandlerSpec._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala index b4852ce2a..8e2afae47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala @@ -27,14 +27,12 @@ import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.{ChannelCodecs, UpdateAddHtlc} import fr.acinq.eclair.{ShortChannelId, UInt64, randomKey} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner /** * Created by fabrice on 07/02/17. */ -@RunWith(classOf[JUnitRunner]) + class ChannelStateSpec extends FunSuite { import ChannelStateSpec._ 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 99692dfea..67847b1ac 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 @@ -23,13 +23,11 @@ import fr.acinq.eclair.channel.NetworkFeePaid import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} import fr.acinq.eclair.{randomBytes, randomKey} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.compat.Platform -@RunWith(classOf[JUnitRunner]) + class SqliteAuditDbSpec extends FunSuite { def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala index 37da86819..abb910070 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala @@ -20,12 +20,10 @@ import java.sql.DriverManager import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqlitePendingRelayDb} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import org.sqlite.SQLiteException -@RunWith(classOf[JUnitRunner]) + class SqliteChannelsDbSpec extends FunSuite { def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") 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 3568fba6d..b44732182 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 @@ -21,15 +21,13 @@ import java.sql.DriverManager import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb -import fr.acinq.eclair.{ShortChannelId, randomKey} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.Color -import org.junit.runner.RunWith +import fr.acinq.eclair.{ShortChannelId, randomKey} import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import org.sqlite.SQLiteException -@RunWith(classOf[JUnitRunner]) + class SqliteNetworkDbSpec extends FunSuite { def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") 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 52211208e..aaa4ea61b 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 @@ -20,11 +20,9 @@ import java.sql.DriverManager import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class SqlitePaymentsDbSpec extends FunSuite { def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala index cc81fa625..09461f6a2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala @@ -21,11 +21,9 @@ import java.sql.DriverManager import fr.acinq.eclair.db.sqlite.SqlitePeersDb import fr.acinq.eclair.randomKey -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class SqlitePeersDbSpec extends FunSuite { def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala index cb4d975dc..8653dcd16 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala @@ -22,11 +22,9 @@ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULF import fr.acinq.eclair.db.sqlite.SqlitePendingRelayDb import fr.acinq.eclair.randomBytes import fr.acinq.eclair.wire.FailureMessageCodecs -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class SqlitePendingRelayDbSpec extends FunSuite { def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") 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 4552dd4fb..d21bd7b84 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 @@ -44,8 +44,6 @@ import fr.acinq.eclair.{Globals, Kit, Setup} import grizzled.slf4j.Logging import org.json4s.JsonAST.JValue import org.json4s.{DefaultFormats, JString} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.concurrent.Await @@ -55,7 +53,7 @@ import scala.concurrent.duration._ /** * Created by PM on 15/03/2017. */ -@RunWith(classOf[JUnitRunner]) + class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { var nodes: Map[String, Kit] = Map() 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 ddee3188b..5a3ea6c0d 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 @@ -28,9 +28,7 @@ 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 org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner -import org.scalatest.{BeforeAndAfterAll, Matchers, fixture} +import org.scalatest.{BeforeAndAfterAll, Matchers, Outcome, fixture} import scala.concurrent.duration._ import scala.io.Source @@ -38,12 +36,12 @@ import scala.io.Source /** * Created by PM on 30/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fixture.FunSuiteLike with BeforeAndAfterAll { - type FixtureParam = Tuple2[List[String], List[String]] + case class FixtureParam(ref: List[String], res: List[String]) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { Globals.blockCount.set(0) val latch = new CountDownLatch(1) val pipe: ActorRef = system.actorOf(Props(new SynchronizationPipe(latch))) @@ -74,12 +72,12 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix bob2blockchain.expectMsgType[WatchLost] awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) + pipe ! new File(getClass.getResource(s"/scenarii/${test.name}.script").getFile) + latch.await(30, TimeUnit.SECONDS) + val ref = Source.fromFile(getClass.getResource(s"/scenarii/${test.name}.script.expected").getFile).getLines().filterNot(_.startsWith("#")).toList + val res = Source.fromFile(new File(s"${System.getProperty("buildDirectory")}/result.tmp")).getLines().filterNot(_.startsWith("#")).toList + withFixture(test.toNoArgTest(FixtureParam(ref, res))) } - pipe ! new File(getClass.getResource(s"/scenarii/${test.name}.script").getFile) - latch.await(30, TimeUnit.SECONDS) - val ref = Source.fromFile(getClass.getResource(s"/scenarii/${test.name}.script.expected").getFile).getLines().filterNot(_.startsWith("#")).toList - val res = Source.fromFile(new File(s"${System.getProperty("buildDirectory")}/result.tmp")).getLines().filterNot(_.startsWith("#")).toList - test((ref, res)) } override def afterAll { @@ -87,15 +85,15 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix TestKit.shutdownActorSystem(system) } - test("01-offer1") { case (ref, res) => assert(ref === res) } - test("02-offer2") { case (ref, res) => assert(ref === res) } - //test("03-fulfill1") { case (ref, res) => assert(ref === res) } - // test("04-two-commits-onedir") { case (ref, res) => assert(ref === res) } DOES NOT PASS : we now automatically sign back when we receive a revocation and have acked changes - // test("05-two-commits-in-flight") { case (ref, res) => assert(ref === res)} DOES NOT PASS : cannot send two commit in a row (without having first revocation) - test("10-offers-crossover") { case (ref, res) => assert(ref === res) } - test("11-commits-crossover") { case (ref, res) => assert(ref === res) } - /*test("13-fee") { case (ref, res) => assert(ref === res)} - test("14-fee-twice") { case (ref, res) => assert(ref === res)} - test("15-fee-twice-back-to-back") { case (ref, res) => assert(ref === res)}*/ + test("01-offer1") { f => assert(f.ref === f.res) } + test("02-offer2") { f => assert(f.ref === f.res) } + //test("03-fulfill1") { f => assert(f.ref === f.res) } + // test("04-two-commits-onedir") { f => assert(f.ref === f.res) } DOES NOT PASS : we now automatically sign back when we receive a revocation and have acked changes + // test("05-two-commits-in-flight") { f => assert(f.ref === f.res)} DOES NOT PASS : cannot send two commit in a row (without having first revocation) + test("10-offers-crossover") { f => assert(f.ref === f.res) } + test("11-commits-crossover") { f => assert(f.ref === f.res) } + /*test("13-fee") { f => assert(f.ref === f.res)} + test("14-fee-twice") { f => assert(f.ref === f.res)} + test("15-fee-twice-back-to-back") { f => assert(f.ref === f.res)}*/ } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala index 7c2520e79..8b02a58e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala @@ -16,11 +16,9 @@ package fr.acinq.eclair.io -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class NodeURISpec extends FunSuite { val PUBKEY = "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134" 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 55a9cf9b4..a0b196985 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 @@ -12,13 +12,11 @@ import fr.acinq.eclair.io.Peer.ResumeAnnouncements import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, wire} -import org.junit.runner.RunWith import org.scalatest.Outcome -import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ -@RunWith(classOf[JUnitRunner]) + class PeerSpec extends TestkitBaseClass { val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(100) val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) @@ -26,7 +24,7 @@ class PeerSpec extends TestkitBaseClass { val updates = (fakeRoutingInfo.map(_._2) ++ fakeRoutingInfo.map(_._3)).toList val nodes = (fakeRoutingInfo.map(_._4) ++ fakeRoutingInfo.map(_._5)).toList - override type FixtureParam = Tuple8[PublicKey, TestProbe, TestProbe, TestProbe, TestProbe, TestProbe, TestProbe, ActorRef] + case class FixtureParam(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef) override protected def withFixture(test: OneArgTest): Outcome = { val authenticator = TestProbe() @@ -38,7 +36,7 @@ class PeerSpec extends TestkitBaseClass { val wallet: EclairWallet = null // unused val remoteNodeId = Bob.nodeParams.nodeId val peer = system.actorOf(Peer.props(Alice.nodeParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) - test((remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer)) + withFixture(test.toNoArgTest(FixtureParam(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer))) } def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef): Unit = { @@ -55,7 +53,8 @@ class PeerSpec extends TestkitBaseClass { assert(probe.expectMsgType[Peer.PeerInfo].state == "CONNECTED") } - test("filter gossip message (no filtering)") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + test("filter gossip message (no filtering)") { f => + import f._ val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap) @@ -65,7 +64,8 @@ class PeerSpec extends TestkitBaseClass { nodes.foreach(transport.expectMsg(_)) } - test("filter gossip message (filtered by origin)") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + test("filter gossip message (filtered by origin)") { f => + import f._ val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) val rebroadcast = Rebroadcast( @@ -79,7 +79,8 @@ class PeerSpec extends TestkitBaseClass { (nodes.toSet - nodes(4)).foreach(transport.expectMsg(_)) } - test("filter gossip message (filtered by timestamp)") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + test("filter gossip message (filtered by timestamp)") { f => + import f._ val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap) @@ -89,12 +90,13 @@ class PeerSpec extends TestkitBaseClass { probe.send(peer, rebroadcast) // peer doesn't filter channel announcements channels.foreach(transport.expectMsg(_)) - // but it will only send updates and node annoucements matching the filter + // but it will only send updates and node announcements matching the filter updates.filter(u => timestamps.contains(u.timestamp)).foreach(transport.expectMsg(_)) nodes.filter(u => timestamps.contains(u.timestamp)).foreach(transport.expectMsg(_)) } - test("react to peer's bad behavior") { case (remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) => + test("react to peer's bad behavior") { f => + import f._ val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) 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 0b6fdfb6a..88fbc96ff 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 @@ -25,9 +25,7 @@ import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload} import fr.acinq.eclair.{ShortChannelId, TestConstants, nodeFee, randomBytes} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scodec.bits.BitVector import scala.util.Success @@ -35,7 +33,7 @@ import scala.util.Success /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class HtlcGenerationSpec extends FunSuite { test("compute fees") { 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 991517cbc..1ca28af3c 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 @@ -26,16 +26,14 @@ import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} -import org.junit.runner.RunWith import org.scalatest.FunSuiteLike -import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ /** * Created by PM on 24/03/2017. */ -@RunWith(classOf[JUnitRunner]) + class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { test("LocalPaymentHandler should reply with a fulfill/fail, emit a PaymentReceived and adds payment in DB") { 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 b93d5ef3c..70fdb5b60 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 @@ -21,21 +21,19 @@ import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{BinaryData, Block, MilliSatoshi} import fr.acinq.eclair.Globals -import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} import fr.acinq.eclair.channel.Register.ForwardShortId +import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.ErrorPacket import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.makeChannelUpdate import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner /** * Created by PM on 29/08/2016. */ -@RunWith(classOf[JUnitRunner]) + class PaymentLifecycleSpec extends BaseRouterSpec { val initialBlockCount = 420000 @@ -44,7 +42,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val defaultAmountMsat = 142000000L val defaultPaymentHash = BinaryData("42" * 32) - test("payment failed (route not found)") { case (router, _) => + test("payment failed (route not found)") { fixture => + import fixture._ val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -59,7 +58,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.expectMsg(PaymentFailed(request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) } - test("payment failed (route too expensive)") { case (router, _) => + test("payment failed (route too expensive)") { fixture => + import fixture._ val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -74,7 +74,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Seq(LocalFailure(RouteTooExpensive(_, _))) = sender.expectMsgType[PaymentFailed].failures } - test("payment failed (unparsable failure)") { case (router, _) => + test("payment failed (unparsable failure)") { fixture => + import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) @@ -111,7 +112,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.expectMsg(PaymentFailed(request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) } - test("payment failed (local error)") { case (router, _) => + test("payment failed (local error)") { fixture => + import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) @@ -138,7 +140,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) } - test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { case (router, _) => + test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => + import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) @@ -165,7 +168,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) } - test("payment failed (TemporaryChannelFailure)") { case (router, _) => + test("payment failed (TemporaryChannelFailure)") { fixture => + import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) @@ -201,7 +205,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } - test("payment failed (Update)") { case (router, _) => + test("payment failed (Update)") { fixture => + import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) @@ -257,7 +262,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) } - test("payment failed (PermanentChannelFailure)") { case (router, _) => + test("payment failed (PermanentChannelFailure)") { fixture => + import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) @@ -290,7 +296,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } - test("payment succeeded") { case (router, _) => + test("payment succeeded") { fixture => + import fixture._ val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -313,7 +320,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) } - test("filter errors properly") { case _ => + test("filter errors properly") { fixture => val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed("00" * 32, "00" * 32, ChannelUnavailable("00" * 32), Local(None), None, None)) :: LocalFailure(RouteNotFound) :: Nil val filtered = PaymentLifecycle.transformForUser(failures) assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable("00" * 32)) :: Nil) 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 58a08b185..de00748fd 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 @@ -22,14 +22,12 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Bech32, BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.payment.PaymentRequest._ -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner /** * Created by fabrice on 15/05/17. */ -@RunWith(classOf[JUnitRunner]) + class PaymentRequestSpec extends FunSuite { val priv = PrivateKey(BinaryData("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734"), compressed = true) 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 a71cbcbbe..c4ae11a6f 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 @@ -26,31 +26,29 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes, randomKey} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ /** * Created by PM on 29/08/2016. */ -@RunWith(classOf[JUnitRunner]) + class RelayerSpec extends TestkitBaseClass { // let's reuse the existing test data import HtlcGenerationSpec._ - type FixtureParam = Tuple3[ActorRef, TestProbe, TestProbe] - - override def withFixture(test: OneArgTest) = { + case class FixtureParam(relayer: ActorRef, register: TestProbe, paymentHandler: TestProbe) + override def withFixture(test: OneArgTest): Outcome = { within(30 seconds) { val register = TestProbe() val paymentHandler = TestProbe() // we are node B in the route A -> B -> C -> .... //val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams.copy(nodeKey = priv_b), register.ref, paymentHandler.ref)) val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams, register.ref, paymentHandler.ref)) - test((relayer, register, paymentHandler)) + withFixture(test.toNoArgTest(FixtureParam(relayer, register, paymentHandler))) } } @@ -61,7 +59,8 @@ class RelayerSpec extends TestkitBaseClass { RemoteCommit(42, CommitmentSpec(Set.empty, 20000, 5000000, 100000000), "00" * 32, randomKey.toPoint), null, null, 0, 0, Map.empty, null, null, null, channelId) - test("relay an htlc-add") { case (relayer, register, paymentHandler) => + test("relay an htlc-add") { f => + import f._ val sender = TestProbe() // we use this to build a valid onion @@ -80,7 +79,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when we have no channel_update for the next channel") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when we have no channel_update for the next channel") { f => + import f._ val sender = TestProbe() // we use this to build a valid onion @@ -99,7 +99,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when register returns an error") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when register returns an error") { f => + import f._ val sender = TestProbe() // we use this to build a valid onion @@ -125,7 +126,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when the channel is advertised as unusable (down)") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when the channel is advertised as unusable (down)") { f => + import f._ val sender = TestProbe() // check that payments are sent properly @@ -143,7 +145,7 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) // now tell the relayer that the channel is down and try again - relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId) + relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId) val (cmd1, _) = buildCommand(finalAmountMsat, finalExpiry, "02" * 32, hops) val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.expiry, cmd1.onion) @@ -157,7 +159,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when the requested channel is disabled") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when the requested channel is disabled") { f => + import f._ val sender = TestProbe() // we use this to build a valid onion @@ -177,7 +180,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when the onion is malformed") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when the onion is malformed") { f => + import f._ val sender = TestProbe() // we use this to build a valid onion @@ -196,7 +200,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when amount is below the next hop's requirements") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when amount is below the next hop's requirements") { f => + import f._ val sender = TestProbe() // we use this to build a valid onion @@ -215,7 +220,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when expiry does not match next hop's requirements") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when expiry does not match next hop's requirements") { f => + import f._ val sender = TestProbe() val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0))) @@ -234,7 +240,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail to relay an htlc-add when relay fee isn't sufficient") { case (relayer, register, paymentHandler) => + test("fail to relay an htlc-add when relay fee isn't sufficient") { f => + import f._ val sender = TestProbe() val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(feeBaseMsat = hops(1).lastUpdate.feeBaseMsat / 2))) @@ -253,7 +260,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail an htlc-add at the final node when amount has been modified by second-to-last node") { case (relayer, register, paymentHandler) => + test("fail an htlc-add at the final node when amount has been modified by second-to-last node") { f => + import f._ val sender = TestProbe() // to simulate this we use a zero-hop route A->B where A is the 'attacker' @@ -273,7 +281,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("fail an htlc-add at the final node when expiry has been modified by second-to-last node") { case (relayer, register, paymentHandler) => + test("fail an htlc-add at the final node when expiry has been modified by second-to-last node") { f => + import f._ val sender = TestProbe() // to simulate this we use a zero-hop route A->B where A is the 'attacker' @@ -293,7 +302,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("correctly translates errors returned by channel when attempting to add an htlc") { case (relayer, register, paymentHandler) => + test("correctly translates errors returned by channel when attempting to add an htlc") { f => + import f._ val sender = TestProbe() val paymentHash = randomBytes(32) @@ -322,7 +332,8 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } - test("relay an htlc-fulfill") { case (relayer, register, _) => + test("relay an htlc-fulfill") { f => + import f._ val sender = TestProbe() val eventListener = TestProbe() @@ -342,7 +353,8 @@ class RelayerSpec extends TestkitBaseClass { assert(paymentRelayed.copy(timestamp = 0) === PaymentRelayed(MilliSatoshi(origin.amountMsatIn), MilliSatoshi(origin.amountMsatOut), add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0)) } - test("relay an htlc-fail") { case (relayer, register, _) => + test("relay an htlc-fail") { f => + import f._ val sender = TestProbe() // we build a fake htlc for the downstream channel 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 d4ec6d5a2..b3b2a8ffa 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 @@ -17,8 +17,8 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem -import akka.testkit.TestProbe import akka.pattern.pipe +import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction} import fr.acinq.eclair.blockchain.ValidateResult @@ -27,9 +27,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, Exten import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} import fr.acinq.eclair.{ShortChannelId, randomKey} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext} @@ -37,7 +35,7 @@ import scala.concurrent.{Await, ExecutionContext} /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class AnnouncementsBatchValidationSpec extends FunSuite { import AnnouncementsBatchValidationSpec._ 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 aedc401cd..998b69d58 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 @@ -21,14 +21,12 @@ import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements._ -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class AnnouncementsSpec extends FunSuite { test("check nodeId1/nodeId2 lexical ordering") { 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 355f8a51d..bbe2c2ea0 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 @@ -28,8 +28,7 @@ import fr.acinq.eclair.router.Announcements._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestkitBaseClass, randomKey, _} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner +import org.scalatest.Outcome import scala.concurrent.duration._ @@ -38,12 +37,10 @@ import scala.concurrent.duration._ * It is re-used in payment FSM tests * Created by PM on 29/08/2016. */ -@RunWith(classOf[JUnitRunner]) + abstract class BaseRouterSpec extends TestkitBaseClass { - import BaseRouterSpec._ - - type FixtureParam = Tuple2[ActorRef, TestProbe] + case class FixtureParam(router: ActorRef, watcher: TestProbe) val remoteNodeId = PrivateKey(BinaryData("01" * 32), compressed = true).publicKey @@ -87,7 +84,7 @@ abstract class BaseRouterSpec extends TestkitBaseClass { val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, cltvExpiryDelta = 9, 0, feeBaseMsat = 786000, feeProportionalMillionths = 8) val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, cltvExpiryDelta = 9, 0, feeBaseMsat = 786000, feeProportionalMillionths = 8) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) within(30 seconds) { @@ -151,7 +148,7 @@ abstract class BaseRouterSpec extends TestkitBaseClass { nodes.size === 6 && channels.size === 4 && updates.size === 8 }, max = 10 seconds, interval = 1 second) - test((router, watcher)) + withFixture(test.toNoArgTest(FixtureParam(router, watcher))) } } } 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 78fe9a72d..4e8845072 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 @@ -3,13 +3,11 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Block import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.wire.ReplyChannelRange -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.collection.{SortedSet, immutable} -@RunWith(classOf[JUnitRunner]) + class ChannelRangeQueriesSpec extends FunSuite { import ChannelRangeQueriesSpec._ 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 d12c75388..865d466eb 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 @@ -20,19 +20,16 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} +import fr.acinq.eclair.{ShortChannelId, randomKey} import org.jgrapht.graph.DirectedWeightedPseudograph -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -import scala.compat.Platform import scala.util.{Failure, Success} /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class RouteCalculationSpec extends FunSuite { import RouteCalculationSpec._ 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 048c7eaa1..edcf287ae 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 @@ -27,10 +27,8 @@ import fr.acinq.eclair.io.Peer.{InvalidSignature, 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, QueryShortChannelIds} +import fr.acinq.eclair.wire.QueryShortChannelIds import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner import scala.collection.SortedSet import scala.compat.Platform @@ -39,10 +37,11 @@ import scala.concurrent.duration._ /** * Created by PM on 29/08/2016. */ -@RunWith(classOf[JUnitRunner]) + class RouterSpec extends BaseRouterSpec { - test("properly announce valid new channels and ignore invalid ones") { case (router, watcher) => + test("properly announce valid new channels and ignore invalid ones") { fixture => + import fixture._ val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent]) @@ -87,7 +86,8 @@ class RouterSpec extends BaseRouterSpec { eventListener.expectMsg(ChannelDiscovered(chan_ac, Satoshi(1000000))) } - test("properly announce lost channels and nodes") { case (router, _) => + test("properly announce lost channels and nodes") { fixture => + import fixture._ val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent]) @@ -111,7 +111,8 @@ class RouterSpec extends BaseRouterSpec { } - test("handle bad signature for ChannelAnnouncement") { case (router, _) => + test("handle bad signature for ChannelAnnouncement") { fixture => + import fixture._ val sender = TestProbe() val channelId_ac = ShortChannelId(420000, 5, 0) val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c) @@ -121,7 +122,8 @@ class RouterSpec extends BaseRouterSpec { sender.expectMsg(InvalidSignature(buggy_chan_ac)) } - test("handle bad signature for NodeAnnouncement") { case (router, _) => + test("handle bad signature for NodeAnnouncement") { fixture => + import fixture._ val sender = TestProbe() val buggy_ann_a = ann_a.copy(signature = ann_b.signature, timestamp = ann_a.timestamp + 1) sender.send(router, PeerRoutingMessage(null, remoteNodeId, buggy_ann_a)) @@ -129,7 +131,8 @@ class RouterSpec extends BaseRouterSpec { sender.expectMsg(InvalidSignature(buggy_ann_a)) } - test("handle bad signature for ChannelUpdate") { case (router, _) => + test("handle bad signature for ChannelUpdate") { fixture => + import fixture._ val sender = TestProbe() val buggy_channelUpdate_ab = channelUpdate_ab.copy(signature = ann_b.signature, timestamp = channelUpdate_ab.timestamp + 1) sender.send(router, PeerRoutingMessage(null, remoteNodeId, buggy_channelUpdate_ab)) @@ -137,28 +140,32 @@ class RouterSpec extends BaseRouterSpec { sender.expectMsg(InvalidSignature(buggy_channelUpdate_ab)) } - test("route not found (unreachable target)") { case (router, _) => + test("route not found (unreachable target)") { fixture => + import fixture._ val sender = TestProbe() // no route a->f sender.send(router, RouteRequest(a, f)) sender.expectMsg(Failure(RouteNotFound)) } - test("route not found (non-existing source)") { case (router, _) => + test("route not found (non-existing source)") { fixture => + import fixture._ val sender = TestProbe() // no route a->f sender.send(router, RouteRequest(randomKey.publicKey, f)) sender.expectMsg(Failure(RouteNotFound)) } - test("route not found (non-existing target)") { case (router, _) => + test("route not found (non-existing target)") { fixture => + import fixture._ val sender = TestProbe() // no route a->f sender.send(router, RouteRequest(a, randomKey.publicKey)) sender.expectMsg(Failure(RouteNotFound)) } - test("route found") { case (router, _) => + test("route found") { fixture => + import fixture._ val sender = TestProbe() sender.send(router, RouteRequest(a, d)) val res = sender.expectMsgType[RouteResponse] @@ -166,7 +173,8 @@ class RouterSpec extends BaseRouterSpec { assert(res.hops.last.nextNodeId === d) } - test("route found (with extra routing info)") { case (router, _) => + test("route found (with extra routing info)") { fixture => + import fixture._ val sender = TestProbe() val x = randomKey.publicKey val y = randomKey.publicKey @@ -180,7 +188,8 @@ class RouterSpec extends BaseRouterSpec { assert(res.hops.last.nextNodeId === z) } - test("route not found (channel disabled)") { case (router, _) => + test("route not found (channel disabled)") { fixture => + import fixture._ val sender = TestProbe() sender.send(router, RouteRequest(a, d)) val res = sender.expectMsgType[RouteResponse] @@ -194,7 +203,8 @@ class RouterSpec extends BaseRouterSpec { sender.expectMsg(Failure(RouteNotFound)) } - test("temporary channel exclusion") { case (router, _) => + test("temporary channel exclusion") { fixture => + import fixture._ val sender = TestProbe() sender.send(router, RouteRequest(a, d)) sender.expectMsgType[RouteResponse] @@ -212,7 +222,8 @@ class RouterSpec extends BaseRouterSpec { sender.expectMsgType[RouteResponse] } - test("export graph in dot format") { case (router, _) => + test("export graph in dot format") { fixture => + import fixture._ val sender = TestProbe() sender.send(router, 'dot) val dot = sender.expectMsgType[String] @@ -226,7 +237,8 @@ class RouterSpec extends BaseRouterSpec { Files.write(img, new File("graph.png"))*/ } - test("send routing state") { case (router, _) => + test("send routing state") { fixture => + import fixture._ val sender = TestProbe() sender.send(router, GetRoutingState) val state = sender.expectMsgType[RoutingState] @@ -235,7 +247,8 @@ class RouterSpec extends BaseRouterSpec { assert(state.updates.size == 8) } - test("ask for channels that we marked as stale for which we receive a new update") { case (router, watcher) => + test("ask for channels that we marked as stale for which we receive a new update") { fixture => + import fixture._ val blockHeight = Globals.blockCount.get().toInt - 2020 val channelId = ShortChannelId(blockHeight, 5, 0) val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) 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 7c4fdb05f..f21f3b963 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 @@ -10,13 +10,11 @@ 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.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 TestKit(ActorSystem("test")) with FunSuiteLike { import RoutingSyncSpec.makeFakeRoutingInfo @@ -37,7 +35,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - // split our anwser in 3 blocks + // 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) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala index 0f4365488..2040980a6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala @@ -19,11 +19,9 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin._ import fr.acinq.eclair.transactions.Scripts._ -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class ClaimReceivedHtlcSpec extends FunSuite { object Alice { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala index 3203d3b86..e0dff133d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala @@ -19,11 +19,9 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin._ import fr.acinq.eclair.transactions.Scripts._ -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + class ClaimSentHtlcSpec extends FunSuite { object Alice { 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 d3bfc6ad4..02dcf93a8 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 @@ -18,11 +18,9 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.{BinaryData, Crypto} import fr.acinq.eclair.wire.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner -@RunWith(classOf[JUnitRunner]) + 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) 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 de1e5d6db..e8f2d537e 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 @@ -26,9 +26,7 @@ import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalD import fr.acinq.eclair.transactions.Transactions.{addSigs, _} import fr.acinq.eclair.wire.UpdateAddHtlc import grizzled.slf4j.Logging -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.io.Source import scala.util.{Failure, Random, Success, Try} @@ -36,7 +34,7 @@ import scala.util.{Failure, Random, Success, Try} /** * Created by PM on 16/12/2016. */ -@RunWith(classOf[JUnitRunner]) + class TransactionsSpec extends FunSuite with Logging { test("encode/decode sequence and locktime (one example)") { 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 a4628e800..40d328c3e 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 @@ -24,16 +24,14 @@ import fr.acinq.eclair.payment.{Local, Relayed} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.ChannelCodecs._ import fr.acinq.eclair.{UInt64, randomKey} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scala.util.Random /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class ChannelCodecsSpec extends FunSuite { def randomBytes(size: Int): BinaryData = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommandCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommandCodecsSpec.scala index dcf4356ea..e3d617d79 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommandCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommandCodecsSpec.scala @@ -18,14 +18,12 @@ package fr.acinq.eclair.wire import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC, Command} import fr.acinq.eclair.randomBytes -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class CommandCodecsSpec extends FunSuite { test("encode/decode all channel messages") { 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 a76c2c409..6e6c7ebb7 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,9 +18,7 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.eclair.ShortChannelId -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scodec.bits.BitVector import scala.util.Random @@ -28,7 +26,7 @@ import scala.util.Random /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class FailureMessageCodecsSpec extends FunSuite { val channelUpdate = ChannelUpdate( signature = BinaryData("3045022100c451cd65c88f55b1767941a247e849e12f5f4d4a93a07316659e22f5267d2088022009042a595c6bc8942cd9d729317b82b306edc259fb6b3a3cecb3dd1bd446e90601"), 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 3be0e8f9a..fb32f78d9 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 @@ -24,15 +24,13 @@ import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire.LightningMessageCodecs._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomBytes, randomKey} -import org.junit.runner.RunWith import org.scalatest.FunSuite -import org.scalatest.junit.JUnitRunner import scodec.bits.{BitVector, ByteVector, HexStringSyntax} /** * Created by PM on 31/05/2016. */ -@RunWith(classOf[JUnitRunner]) + class LightningMessageCodecsSpec extends FunSuite { import LightningMessageCodecsSpec._ diff --git a/pom.xml b/pom.xml index 815baac25..26f18405c 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,7 @@ net.alchim31.maven scala-maven-plugin - 3.3.1 + 3.4.2 -deprecation @@ -193,26 +193,35 @@ + org.apache.maven.plugins maven-surefire-plugin 2.20 - -Xmx1024m - false - false - - - - **/*Spec.* - **/*Test.* - **/*Suite.* - - - ${project.build.directory} - + true + + + org.scalatest + scalatest-maven-plugin + 2.0.0 + + false + + ${project.build.directory} + + + + + test + + test + + + + @@ -229,16 +238,10 @@ scala-library ${scala.version} - - junit - junit - 4.12 - test - org.scalatest scalatest_${scala.version.short} - 2.2.6 + 3.0.5 test From ac791d955d1847b98e3acb132ac932596be9d8a5 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Mon, 15 Oct 2018 14:28:19 +0200 Subject: [PATCH 16/31] Add instructions for Bitcoin Core 0.17.0 [ci skip] (#732) * Add instructions for Bitcoin Core 0.17.0 [ci skip] Bitcoin Core 0.17.0 deprecates the `signrawtransaction` RPC call, which will be removed in version 0.18.0, you need to enable this call if you want your eclair node to use a 0.1.70 node. * README: add an example of how to use the new bitcoin.conf sections [ci skip] --- README.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0c8dc64ba..17d28b3e2 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Please see the latest [release note](https://github.com/ACINQ/eclair/releases) f ### Configuring Bitcoin Core -:warning: Eclair requires Bitcoin Core 0.16.0 or higher. If you are upgrading an existing wallet, you need to create a new address and send all your funds to that address. +:warning: Eclair requires Bitcoin Core 0.16.3 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 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. @@ -47,6 +47,11 @@ zmqpubrawtx=tcp://127.0.0.1:29000 addresstype=p2sh-segwit ``` +:warning: If you are using Bitcoin Core 0.17.0 you need to add following line to your `bitcoin.conf`: +``` +deprecatedrpc=signrawtransaction +``` + ### Installing Eclair The released binaries can be downloaded [here](https://github.com/ACINQ/eclair/releases). @@ -198,16 +203,39 @@ zmqpubrawtx=tcp://127.0.0.1:29000 addresstype=p2sh-segwit ``` +:warning: If you are using Bitcoin Core 0.17.0 you need to add following line to your `bitcoin.conf`: +``` +deprecatedrpc=signrawtransaction +``` + +You may also want to take advantage of the new configuration sections in `bitcoin.conf` to manage parameters that are network speficic, so you can reasliy run your bitcoin node on both mainnet and testnet. For example you could use: + +``` +server=1 +txindex=1 +addresstype=p2sh-segwit +deprecatedrpc=signrawtransaction +[main] +rpcuser= +rpcpassword= +zmqpubrawblock=tcp://127.0.0.1:29000 +zmqpubrawtx=tcp://127.0.0.1:29000 +[test] +rpcuser= +rpcpassword= +zmqpubrawblock=tcp://127.0.0.1:29001 +zmqpubrawtx=tcp://127.0.0.1:29001 +``` + ### Eclair configuration ``` eclair.chain=mainnet eclair.bitcoind.rpcport=8332 -eclair.bitcoind.rpcuser= -eclair.bitcoind.rpcpassword= +eclair.bitcoind.rpcuser= +eclair.bitcoind.rpcpassword= ``` - ## 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 From 22b6bfb0abce4f7f27f91be625e129f0a0beb20d Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 15 Oct 2018 14:46:51 +0200 Subject: [PATCH 17/31] Only persist trimmed htlcs (#724) 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. Removed the TODO because we need data to be indexed by commit number so it is ok to write the same htlc data for every commitment it is included in. --- .../fr/acinq/eclair/channel/Channel.scala | 6 ++- .../fr/acinq/eclair/channel/Helpers.scala | 2 +- .../scala/fr/acinq/eclair/db/ChannelsDb.scala | 2 +- .../eclair/db/sqlite/SqliteChannelsDb.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 49 +++++++++++++++++++ .../eclair/db/SqliteChannelsDbSpec.scala | 8 +-- 6 files changed, 60 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 64d649897..12b47ee1a 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 @@ -608,10 +608,12 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case u: UpdateFailHtlc => relayer ! CommandBuffer.CommandAck(u.channelId, u.id) case u: UpdateFailMalformedHtlc => relayer ! CommandBuffer.CommandAck(u.channelId, u.id) } - // TODO: be smarter and only consider commitments1.localChanges.signed and commitments1.remoteChanges.signed val nextRemoteCommit = commitments1.remoteNextCommitInfo.left.get.nextRemoteCommit val nextCommitNumber = nextRemoteCommit.index - nextRemoteCommit.spec.htlcs collect { + // 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) + trimmedHtlcs collect { case DirectedHtlc(_, u) => log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.expiry} to htlcs db for commitNumber=$nextCommitNumber") nodeParams.channelsDb.addOrUpdateHtlcInfo(d.channelId, nextCommitNumber, u.paymentHash, u.expiry) 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 4e7c82aec..e369f2ef7 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 @@ -558,7 +558,7 @@ object Helpers { }) // we retrieve the informations needed to rebuild htlc scripts - val htlcInfos = db.listHtlcHtlcInfos(commitments.channelId, txnumber) + val htlcInfos = db.listHtlcInfos(commitments.channelId, txnumber) log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txnumber") val htlcsRedeemScripts = ( htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry) } ++ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index 63ea93bc5..07d4f87d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -29,6 +29,6 @@ trait ChannelsDb { def addOrUpdateHtlcInfo(channelId: BinaryData, commitmentNumber: Long, paymentHash: BinaryData, cltvExpiry: Long) - def listHtlcHtlcInfos(channelId: BinaryData, commitmentNumber: Long): Seq[(BinaryData, Long)] + def listHtlcInfos(channelId: BinaryData, commitmentNumber: Long): Seq[(BinaryData, Long)] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index 1a8f4a0b5..e81cea31d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -89,7 +89,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { } } - def listHtlcHtlcInfos(channelId: BinaryData, commitmentNumber: Long): Seq[(BinaryData, Long)] = { + def listHtlcInfos(channelId: BinaryData, commitmentNumber: Long): Seq[(BinaryData, Long)] = { using(sqlite.prepareStatement("SELECT payment_hash, cltv_expiry FROM htlc_infos WHERE channel_id=? AND commitment_number=?")) { statement => statement.setBytes(1, channelId) statement.setLong(2, commitmentNumber) 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 1d888b1b3..ff506f73e 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 @@ -32,6 +32,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.transactions.Transactions.{htlcSuccessWeight, htlcTimeoutWeight, weight2fee} import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -461,6 +462,54 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(commitSig.htlcSignatures.toSet.size == 4) } + test("recv CMD_SIGN (check htlc info are persisted)") { f => + import f._ + val sender = TestProbe() + // for the test to be really useful we have constraint on parameters + assert(Alice.nodeParams.dustLimitSatoshis > Bob.nodeParams.dustLimitSatoshis) + // 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 + 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, "11" * 32, 400144)) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, "22" * 32, 400144)) + sender.expectMsg("ok") + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, "33" * 32, 400144)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateAddHtlc] + bob2alice.forward(alice) + sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, "44" * 32, 400144)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateAddHtlc] + bob2alice.forward(alice) + + // actual test starts here + crossSign(alice, bob, alice2bob, bob2alice) + // depending on who starts signing first, there will be one or two commitments because both sides have changes + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 2) + assert(alice.underlyingActor.nodeParams.channelsDb.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 0).size == 0) + assert(alice.underlyingActor.nodeParams.channelsDb.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 1).size == 2) + assert(alice.underlyingActor.nodeParams.channelsDb.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 2).size == 4) + assert(bob.underlyingActor.nodeParams.channelsDb.listHtlcInfos(bob.stateData.asInstanceOf[DATA_NORMAL].channelId, 0).size == 0) + assert(bob.underlyingActor.nodeParams.channelsDb.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() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala index abb910070..9e9b511ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala @@ -54,15 +54,15 @@ class SqliteChannelsDbSpec extends FunSuite { db.addOrUpdateChannel(channel) assert(db.listChannels() === List(channel)) - assert(db.listHtlcHtlcInfos(channel.channelId, commitNumber).toList == Nil) + assert(db.listHtlcInfos(channel.channelId, commitNumber).toList == Nil) db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1) db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash2, cltvExpiry2) - assert(db.listHtlcHtlcInfos(channel.channelId, commitNumber).toList == List((paymentHash1, cltvExpiry1), (paymentHash2, cltvExpiry2))) - assert(db.listHtlcHtlcInfos(channel.channelId, 43).toList == Nil) + assert(db.listHtlcInfos(channel.channelId, commitNumber).toList == List((paymentHash1, cltvExpiry1), (paymentHash2, cltvExpiry2))) + assert(db.listHtlcInfos(channel.channelId, 43).toList == Nil) db.removeChannel(channel.channelId) assert(db.listChannels() === Nil) - assert(db.listHtlcHtlcInfos(channel.channelId, commitNumber).toList == Nil) + assert(db.listHtlcInfos(channel.channelId, commitNumber).toList == Nil) } } From c564f51d242bf0b0e2ca42e1ab9eed9be323c001 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 15 Oct 2018 16:48:13 +0200 Subject: [PATCH 18/31] set version to 0.2-beta7 --- eclair-core/pom.xml | 2 +- eclair-node-gui/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index a563e880b..820bc2a1f 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta7 eclair-core_2.11 diff --git a/eclair-node-gui/pom.xml b/eclair-node-gui/pom.xml index 293177cee..5f912e4d3 100644 --- a/eclair-node-gui/pom.xml +++ b/eclair-node-gui/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta7 eclair-node-gui_2.11 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 36c687f81..3a6f0f52b 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta7 eclair-node_2.11 diff --git a/pom.xml b/pom.xml index 26f18405c..0c904432a 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta7 pom From b0305b0551d04a52429a9841b193196b61e78f69 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 15 Oct 2018 17:00:40 +0200 Subject: [PATCH 19/31] set version to 0.2-SNAPSHOT --- eclair-core/pom.xml | 2 +- eclair-node-gui/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 820bc2a1f..a563e880b 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta7 + 0.2-SNAPSHOT eclair-core_2.11 diff --git a/eclair-node-gui/pom.xml b/eclair-node-gui/pom.xml index 5f912e4d3..293177cee 100644 --- a/eclair-node-gui/pom.xml +++ b/eclair-node-gui/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta7 + 0.2-SNAPSHOT eclair-node-gui_2.11 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 3a6f0f52b..36c687f81 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta7 + 0.2-SNAPSHOT eclair-node_2.11 diff --git a/pom.xml b/pom.xml index 0c904432a..26f18405c 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta7 + 0.2-SNAPSHOT pom From c77c276c775f2376101f80ae03b4e4c653688952 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 19 Oct 2018 17:47:13 +0200 Subject: [PATCH 20/31] Add `htlcMaximumMsat` field to `ChannelUpdate` message (#738) * Add `htlcMaximumMsat` field to `ChannelUpdate` message * added compatibility test with c-lightning --- .../fr/acinq/eclair/channel/Channel.scala | 18 +++++----- .../fr/acinq/eclair/channel/Commitments.scala | 2 +- .../eclair/db/sqlite/SqliteNetworkDb.scala | 4 +-- .../main/scala/fr/acinq/eclair/io/Peer.scala | 6 +++- .../fr/acinq/eclair/payment/Relayer.scala | 6 ++-- .../acinq/eclair/router/Announcements.scala | 36 +++++++++++-------- .../scala/fr/acinq/eclair/router/Router.scala | 17 +++++---- .../fr/acinq/eclair/wire/FailureMessage.scala | 4 +-- .../eclair/wire/LightningMessageCodecs.scala | 23 ++++++------ .../eclair/wire/LightningMessageTypes.scala | 11 ++++-- .../ElectrumWalletSimulatedClientSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 4 +-- .../channel/states/e/OfflineStateSpec.scala | 6 ++-- .../fr/acinq/eclair/db/ChannelStateSpec.scala | 2 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 6 ++-- .../eclair/integration/IntegrationSpec.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 10 +++++- .../eclair/payment/HtlcGenerationSpec.scala | 2 +- .../eclair/payment/PaymentLifecycleSpec.scala | 4 +-- .../fr/acinq/eclair/payment/RelayerSpec.scala | 8 ++--- .../AnnouncementsBatchValidationSpec.scala | 2 +- .../eclair/router/AnnouncementsSpec.scala | 34 +++++++++--------- .../acinq/eclair/router/BaseRouterSpec.scala | 16 ++++----- .../eclair/router/RouteCalculationSpec.scala | 18 +++++----- .../fr/acinq/eclair/router/RouterSpec.scala | 14 ++++---- .../acinq/eclair/router/RoutingSyncSpec.scala | 4 +-- .../wire/FailureMessageCodecsSpec.scala | 10 +++--- .../wire/LightningMessageCodecsSpec.scala | 23 +++++++++--- .../fr/acinq/eclair/gui/GUIUpdater.scala | 8 ++--- 29 files changed, 172 insertions(+), 130 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 12b47ee1a..5ea1b5df2 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 @@ -187,7 +187,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // we rebuild a channel_update for two reasons: // - we want to reload values from configuration // - if eclair was previously killed, it might not have had time to publish a channel_update with enable=false - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, normal.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, normal.commitments.remoteParams.htlcMinimumMsat, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, enable = false) + val channelUpdate = 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 = false) + goto(OFFLINE) using normal.copy(channelUpdate = channelUpdate) case _ => @@ -476,7 +478,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) 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)) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true) + 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 = true) goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None)) case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => @@ -770,7 +772,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // we need to re-announce this shortChannelId context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId)) // we re-announce the channelUpdate for the same reason - Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, enable = true) + Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true) } else d.channelUpdate val localAnnSigs_opt = if (d.commitments.announceChannel) { // if channel is public we need to send our announcement_signatures in order to generate the channel_announcement @@ -813,13 +815,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(TickRefreshChannelUpdate, d: DATA_NORMAL) => // periodic refresh is used as a keep alive log.info(s"sending channel_update announcement (refresh)") - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, enable = true) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true) // we use GOTO instead of stay because we want to fire transitions goto(NORMAL) using store(d.copy(channelUpdate = channelUpdate)) case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) => log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, feeBaseMsat, d.channelUpdate.feeProportionalMillionths, feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, enable = true) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true) // we use GOTO instead of stay because we want to fire transitions goto(NORMAL) using store(d.copy(channelUpdate = channelUpdate)) replying "ok" @@ -832,7 +834,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(INPUT_DISCONNECTED, d: DATA_NORMAL) => // we disable the channel log.debug(s"sending channel_update announcement (disable)") - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, enable = false) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = false) d.commitments.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate), None)) } @@ -1300,7 +1302,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) => log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, feeBaseMsat, d.channelUpdate.feeProportionalMillionths, feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, enable = false) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = false) // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state stay using store(d.copy(channelUpdate = channelUpdate)) replying "ok" @@ -1395,7 +1397,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } } // re-enable the channel - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, enable = true) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true) goto(NORMAL) using d.copy(commitments = commitments1, channelUpdate = channelUpdate) } 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 9aa38e090..b8704aee4 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 @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256} -import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction} +import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.payment.Origin import fr.acinq.eclair.transactions.Transactions._ 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 903f2d83f..37e8edafb 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 @@ -107,7 +107,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { 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.flags)) + statement.setBoolean(2, Announcements.isNode1(u.channelFlags)) statement.setBytes(3, channelUpdateCodec.encode(u).require.toByteArray) statement.executeUpdate() } @@ -117,7 +117,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { 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.flags)) + statement.setBoolean(3, Announcements.isNode1(u.channelFlags)) statement.executeUpdate() } } 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 685410d96..10c078bcc 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.crypto.TransportHandler import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} +import scodec.Attempt import scala.concurrent.duration._ import scala.util.Random @@ -305,7 +306,10 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor case Event(badMessage: BadMessage, data@ConnectedData(_, transport, _, _, _, behavior)) => val behavior1 = badMessage match { case InvalidSignature(r) => - val bin = LightningMessageCodecs.lightningMessageCodec.encode(r) + val bin: String = LightningMessageCodecs.lightningMessageCodec.encode(r) match { + case Attempt.Successful(b) => b.toHex + case _ => "unknown" + } log.error(s"peer sent us a routing message with invalid sig: r=$r bin=$bin") // for now we just return an error, maybe ban the peer in the future? transport ! Error(CHANNELID_ZERO, s"bad announcement sig! bin=$bin".getBytes()) 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 2bf61a4e1..afee2cdfa 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 @@ -137,7 +137,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case (_: ExpiryTooBig, _) => ExpiryTooFar case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate) case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate) - case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.flags) => ChannelDisabled(channelUpdate.flags, channelUpdate) + case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.channelFlags) => ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate) case (_: ChannelUnavailable, None) => PermanentChannelFailure case (_: HtlcTimedout, _) => PermanentChannelFailure case _ => TemporaryNodeFailure @@ -260,8 +260,8 @@ object Relayer { channelUpdate_opt match { case None => Left(CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)) - case Some(channelUpdate) if !Announcements.isEnabled(channelUpdate.flags) => - Left(CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.flags, channelUpdate)), commit = true)) + case Some(channelUpdate) if !Announcements.isEnabled(channelUpdate.channelFlags) => + Left(CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)), commit = true)) case Some(channelUpdate) if payload.amtToForward < channelUpdate.htlcMinimumMsat => Left(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) if relayPayload.expiryDelta != channelUpdate.cltvExpiryDelta => 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 a4d329f49..5de5f8889 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 @@ -37,10 +37,10 @@ object Announcements { sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: HNil)))) def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: BinaryData, addresses: List[NodeAddress]): BinaryData = - sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: (rgbColor) :: alias :: addresses :: HNil)))) + sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: HNil)))) - def channelUpdateWitnessEncode(chainHash: BinaryData, shortChannelId: ShortChannelId, timestamp: Long, flags: BinaryData, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long): BinaryData = - sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: flags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: HNil)))) + def channelUpdateWitnessEncode(chainHash: BinaryData, shortChannelId: ShortChannelId, timestamp: Long, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Option[Long]): BinaryData = + sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: HNil)))) def signChannelAnnouncement(chainHash: BinaryData, shortChannelId: ShortChannelId, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: BinaryData): (BinaryData, BinaryData) = { val witness = if (isNode1(localNodeSecret.publicKey.toBin, remoteNodeId.toBin)) { @@ -108,7 +108,7 @@ object Announcements { * * @return true if the node who sent these flags is node1 */ - def isNode1(flags: BinaryData) = !BitVector(flags.data).reverse.get(0) + def isNode1(channelFlags: Byte): Boolean = (channelFlags & 1) == 0 /** * A node MAY create and send a channel_update with the disable bit set to @@ -116,25 +116,31 @@ object Announcements { * * @return */ - def isEnabled(flags: BinaryData) = !BitVector(flags.data).reverse.get(1) + def isEnabled(channelFlags: Byte): Boolean = (channelFlags & 2) == 0 - def makeFlags(isNode1: Boolean, enable: Boolean): BinaryData = BitVector.bits(!enable :: !isNode1 :: Nil).padLeft(16).toByteArray + def makeMessageFlags(hasOptionChannelHtlcMax: Boolean): Byte = BitVector.bits(hasOptionChannelHtlcMax :: Nil).padLeft(8).toByte() - def makeChannelUpdate(chainHash: BinaryData, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, enable: Boolean = true, timestamp: Long = Platform.currentTime / 1000): ChannelUpdate = { - val flags = makeFlags(isNode1 = isNode1(nodeSecret.publicKey.toBin, remoteNodeId.toBin), enable = enable) - require(flags.size == 2, "flags must be a 2-bytes field") - val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, flags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths) + def makeChannelFlags(isNode1: Boolean, enable: Boolean): Byte = BitVector.bits(!enable :: !isNode1 :: Nil).padLeft(8).toByte() + + def makeChannelUpdate(chainHash: BinaryData, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Long, enable: Boolean = true, timestamp: Long = Platform.currentTime / 1000): ChannelUpdate = { + val messageFlags = makeMessageFlags(hasOptionChannelHtlcMax = true) // NB: we always support option_channel_htlc_max + val channelFlags = makeChannelFlags(isNode1 = isNode1(nodeSecret.publicKey.toBin, remoteNodeId.toBin), enable = enable) + val htlcMaximumMsatOpt = Some(htlcMaximumMsat) + + val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, messageFlags, channelFlags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, htlcMaximumMsatOpt) val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte ChannelUpdate( signature = sig, chainHash = chainHash, shortChannelId = shortChannelId, timestamp = timestamp, - flags = flags, + messageFlags = messageFlags, + channelFlags = channelFlags, cltvExpiryDelta = cltvExpiryDelta, htlcMinimumMsat = htlcMinimumMsat, feeBaseMsat = feeBaseMsat, - feeProportionalMillionths = feeProportionalMillionths + feeProportionalMillionths = feeProportionalMillionths, + htlcMaximumMsat = htlcMaximumMsatOpt ) } @@ -151,8 +157,8 @@ object Announcements { verifySignature(witness, ann.signature, ann.nodeId) } - def checkSig(ann: ChannelUpdate, nodeId: PublicKey): Boolean = { - val witness = channelUpdateWitnessEncode(ann.chainHash, ann.shortChannelId, ann.timestamp, ann.flags, ann.cltvExpiryDelta, ann.htlcMinimumMsat, ann.feeBaseMsat, ann.feeProportionalMillionths) - verifySignature(witness, ann.signature, nodeId) + def checkSig(upd: ChannelUpdate, nodeId: PublicKey): Boolean = { + val witness = channelUpdateWitnessEncode(upd.chainHash, upd.shortChannelId, upd.timestamp, upd.messageFlags, upd.channelFlags, upd.cltvExpiryDelta, upd.htlcMinimumMsat, upd.feeBaseMsat, upd.feeProportionalMillionths, upd.htlcMaximumMsat) + verifySignature(witness, upd.signature, nodeId) } } 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 19c73b8b2..f8ca46f3a 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 @@ -595,7 +595,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct origin ! InvalidSignature(u) d } else if (d.updates.contains(desc)) { - log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.flags, u) + log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdateReceived(u)) db.updateChannelUpdate(u) // we also need to update the graph @@ -603,7 +603,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct addEdge(d.graph, desc, u) d.copy(updates = d.updates + (desc -> u), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> Set(origin)))) } else { - log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.flags, u) + log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdateReceived(u)) db.addChannelUpdate(u) // we also need to update the graph @@ -624,7 +624,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct 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.flags)) ChannelDesc(u.shortChannelId, a, b) else ChannelDesc(u.shortChannelId, b, a) + val desc = if (Announcements.isNode1(u.channelFlags)) ChannelDesc(u.shortChannelId, a, b) else ChannelDesc(u.shortChannelId, b, a) if (isStale(u)) { log.debug("ignoring {} (stale)", u) d @@ -636,14 +636,14 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct origin ! InvalidSignature(u) d } else if (d.privateUpdates.contains(desc)) { - log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.flags, u) + log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdateReceived(u)) // we also need to update the graph removeEdge(d.graph, desc) addEdge(d.graph, desc, u) d.copy(privateUpdates = d.privateUpdates + (desc -> u)) } else { - log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.flags, u) + log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdateReceived(u)) // we also need to update the graph addEdge(d.graph, desc, u) @@ -696,7 +696,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 = "", chainHash = "", extraHop.shortChannelId, Platform.currentTime / 1000, flags = BinaryData("0000"), extraHop.cltvExpiryDelta, htlcMinimumMsat = 0L, extraHop.feeBaseMsat, extraHop.feeProportionalMillionths) + ChannelUpdate(signature = "", chainHash = "", 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] = { // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination @@ -707,9 +707,8 @@ object Router { } def getDesc(u: ChannelUpdate, channel: ChannelAnnouncement): ChannelDesc = { - require(u.flags.data.size == 2, s"invalid flags length ${u.flags.data.size} != 2") // the least significant bit tells us if it is node1 or node2 - if (Announcements.isNode1(u.flags)) ChannelDesc(u.shortChannelId, channel.nodeId1, channel.nodeId2) else ChannelDesc(u.shortChannelId, channel.nodeId2, channel.nodeId1) + if (Announcements.isNode1(u.channelFlags)) ChannelDesc(u.shortChannelId, channel.nodeId1, channel.nodeId2) else ChannelDesc(u.shortChannelId, channel.nodeId2, channel.nodeId1) } def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2 @@ -790,7 +789,7 @@ object Router { * Note that we only add the edge if the corresponding channel is enabled */ def addEdge(g: WeightedGraph[PublicKey, DescEdge], d: ChannelDesc, u: ChannelUpdate) = { - if (Announcements.isEnabled(u.flags)) { + if (Announcements.isEnabled(u.channelFlags)) { g.addVertex(d.a) g.addVertex(d.b) val e = new DescEdge(d, u) 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 bdaf362ed..77e7af173 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 @@ -46,7 +46,7 @@ case object RequiredChannelFeatureMissing extends Perm { def message = "channel 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 ChannelDisabled(flags: BinaryData, update: ChannelUpdate) extends Update { def message = "channel is currently disabled" } +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 object UnknownPaymentHash extends Perm { def message = "payment hash is unknown to the final node" } case object IncorrectPaymentAmount extends Perm { def message = "payment amount is incorrect" } @@ -87,7 +87,7 @@ object FailureMessageCodecs { .typecase(UPDATE | 12, (("amountMsat" | uint64) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient]) .typecase(UPDATE | 13, (("expiry" | uint32) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[IncorrectCltvExpiry]) .typecase(UPDATE | 14, (("channelUpdate" | channelUpdateWithLengthCodec)).as[ExpiryTooSoon]) - .typecase(UPDATE | 20, (("flags" | binarydata(2)) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[ChannelDisabled]) + .typecase(UPDATE | 20, (("messageFlags" | byte) :: ("channelFlags" | byte) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[ChannelDisabled]) .typecase(PERM | 15, provide(UnknownPaymentHash)) .typecase(PERM | 16, provide(IncorrectPaymentAmount)) .typecase(17, provide(FinalExpiryTooSoon)) 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 221a194c0..6b5cea9b2 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 @@ -17,8 +17,7 @@ package fr.acinq.eclair.wire import java.math.BigInteger -import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} - +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} @@ -28,6 +27,7 @@ import fr.acinq.eclair.{ShortChannelId, UInt64, wire} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound} +import shapeless.nat._ import scala.util.{Failure, Success, Try} @@ -280,15 +280,18 @@ object LightningMessageCodecs { ("signature" | signature) :: nodeAnnouncementWitnessCodec).as[NodeAnnouncement] - val channelUpdateWitnessCodec = ( + val channelUpdateWitnessCodec = ("chainHash" | binarydata(32)) :: ("shortChannelId" | shortchannelid) :: ("timestamp" | uint32) :: - ("flags" | binarydata(2)) :: - ("cltvExpiryDelta" | uint16) :: - ("htlcMinimumMsat" | uint64) :: - ("feeBaseMsat" | uint32) :: - ("feeProportionalMillionths" | uint32)) + (("messageFlags" | byte) >>:~ { messageFlags => + ("channelFlags" | byte) :: + ("cltvExpiryDelta" | uint16) :: + ("htlcMinimumMsat" | uint64) :: + ("feeBaseMsat" | uint32) :: + ("feeProportionalMillionths" | uint32) :: + ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, uint64)) + }) val channelUpdateCodec: Codec[ChannelUpdate] = ( ("signature" | signature) :: @@ -322,7 +325,7 @@ object LightningMessageCodecs { ("chainHash" | binarydata(32)) :: ("firstTimestamp" | uint32) :: ("timestampRange" | uint32) - ).as[GossipTimestampFilter] + ).as[GossipTimestampFilter] val lightningMessageCodec = discriminated[LightningMessage].by(uint16) .typecase(16, initCodec) @@ -386,4 +389,4 @@ object LightningMessageCodecs { ("outgoing_cltv_value" | uint32) :: ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] -} +} \ No newline at end of file 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 227bcc4a8..b0ec2489c 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, InetSocketAddress} import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar} import fr.acinq.eclair.{ShortChannelId, UInt64} +import scodec.bits.BitVector /** * Created by PM on 15/11/2016. @@ -192,11 +193,15 @@ case class ChannelUpdate(signature: BinaryData, chainHash: BinaryData, shortChannelId: ShortChannelId, timestamp: Long, - flags: BinaryData, + messageFlags: Byte, + channelFlags: Byte, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, - feeProportionalMillionths: Long) extends RoutingMessage with HasTimestamp with HasChainHash + feeProportionalMillionths: Long, + htlcMaximumMsat: Option[Long]) extends RoutingMessage with HasTimestamp with HasChainHash { + require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") +} case class PerHopPayload(shortChannelId: ShortChannelId, amtToForward: Long, @@ -216,7 +221,7 @@ case class ReplyChannelRange(chainHash: BinaryData, data: BinaryData) extends RoutingMessage with HasChainHash case class ReplyShortChannelIdsEnd(chainHash: BinaryData, - complete: Byte) extends RoutingMessage with HasChainHash + complete: Byte) extends RoutingMessage with HasChainHash case class GossipTimestampFilter(chainHash: BinaryData, firstTimestamp: Long, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala index 6cf45c183..cb919acf7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala @@ -91,7 +91,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit listener.expectMsgType[NewWalletReceiveAddress] } - test("don't send the same ready mnessage more then once") { + test("don't send the same ready message more then once") { // listener should be notified sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(header4)) assert(listener.expectMsgType[WalletReady].timestamp == header4.timestamp) 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 ff506f73e..e963db5b6 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 @@ -1992,14 +1992,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.expectMsgType[AnnouncementSignatures] bob2alice.forward(alice) val update1 = relayer.expectMsgType[LocalChannelUpdate] - assert(Announcements.isEnabled(update1.channelUpdate.flags) == true) + assert(Announcements.isEnabled(update1.channelUpdate.channelFlags) == true) // actual test starts here Thread.sleep(1100) sender.send(alice, INPUT_DISCONNECTED) val update2 = relayer.expectMsgType[LocalChannelUpdate] assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) - assert(Announcements.isEnabled(update2.channelUpdate.flags) == false) + assert(Announcements.isEnabled(update2.channelUpdate.channelFlags) == false) awaitCond(alice.stateName == OFFLINE) } 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 0d678ef76..0226ceab4 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 @@ -334,8 +334,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateName == OFFLINE) // alice and bob announce that their channel is OFFLINE - assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.flags) == false) - assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.flags) == false) + assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags) == false) + assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags) == false) // we make alice update here relay fee sender.send(alice, CMD_UPDATE_RELAY_FEE(4200, 123456)) @@ -358,7 +358,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val channelUpdate = relayer.expectMsgType[LocalChannelUpdate](10 seconds).channelUpdate assert(channelUpdate.feeBaseMsat === 4200) assert(channelUpdate.feeProportionalMillionths === 123456) - assert(Announcements.isEnabled(channelUpdate.flags) == true) + assert(Announcements.isEnabled(channelUpdate.channelFlags) == true) // no more messages relayer.expectNoMsg(300 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala index 8e2afae47..a169779a4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala @@ -107,7 +107,7 @@ object ChannelStateSpec { remoteNextCommitInfo = Right(randomKey.publicKey), commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = "00" * 32) - val channelUpdate = Announcements.makeChannelUpdate("11" * 32, randomKey, randomKey.publicKey, ShortChannelId(142553), 42, 15, 575, 53) + val channelUpdate = Announcements.makeChannelUpdate("11" * 32, randomKey, randomKey.publicKey, ShortChannelId(142553), 42, 15, 575, 53, Channel.MAX_FUNDING_SATOSHIS * 1000L) val normal = DATA_NORMAL(commitments, ShortChannelId(42), true, None, channelUpdate, None, None) } 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 b44732182..d1691abf7 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 @@ -83,9 +83,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, true) - val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(43), 5, 7000000, 50000, 100, true) - val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(44), 5, 7000000, 50000, 100, true) + 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) 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 d21bd7b84..e4e0b317c 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 @@ -242,7 +242,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) + 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) // ...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 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 a0b196985..347d86a4b 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 @@ -8,9 +8,10 @@ import fr.acinq.bitcoin.Crypto.PublicKey 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.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} +import fr.acinq.eclair.wire.Error import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, wire} import org.scalatest.Outcome @@ -156,5 +157,12 @@ class PeerSpec extends TestkitBaseClass { router.expectMsg(Peer.PeerRoutingMessage(transport.ref, remoteNodeId, c)) } transport.expectNoMsg(1 second) // peer hasn't acknowledged the messages + + // let's assume that one of the sigs were invalid + router.send(peer, Peer.InvalidSignature(channels(0))) + // peer will return a connection-wide error, including the hex-encoded representation of the bad message + val error = transport.expectMsgType[Error] + assert(error.channelId === CHANNELID_ZERO) + assert(new String(error.data).startsWith("bad announcement sig! bin=0100")) } } 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 88fbc96ff..fed98ada0 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 @@ -154,7 +154,7 @@ 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.encodeSignature(Crypto.sign(Crypto.sha256(BinaryData.empty), priv_a.privateKey)) :+ 1.toByte - val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, "0000", 0, 42000, 0, 0) + 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) 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 70fdb5b60..5d33d25b6 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 @@ -227,7 +227,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 42, 0, feeBaseMsat = 233000, feeProportionalMillionths = 1) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 42, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(5, channelUpdate_bc_modified) // and node replies with a failure containing a new channel update sender.send(paymentFSM, UpdateFailHtlc("00" * 32, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure))) @@ -244,7 +244,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { relayer.expectMsg(ForwardShortId(channelId_ab, cmd2)) // we change the cltv expiry one more time - val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 43, 0, feeBaseMsat = 233000, feeProportionalMillionths = 1) + val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 43, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure2 = IncorrectCltvExpiry(5, channelUpdate_bc_modified_2) // and node replies with a failure containing a new channel update sender.send(paymentFSM, UpdateFailHtlc("00" * 32, 0, Sphinx.createErrorPacket(sharedSecrets2.head._1, failure2))) 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 c4ae11a6f..7535d6163 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 @@ -167,14 +167,14 @@ class RelayerSpec extends TestkitBaseClass { val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) - val channelUpdate_bc_disabled = channelUpdate_bc.copy(flags = Announcements.makeFlags(Announcements.isNode1(channelUpdate_bc.flags), enable = false)) + 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)) 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(ChannelDisabled(channelUpdate_bc_disabled.flags, channelUpdate_bc_disabled))) + assert(fail.reason == Right(ChannelDisabled(channelUpdate_bc_disabled.messageFlags, channelUpdate_bc_disabled.channelFlags, channelUpdate_bc_disabled))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -318,9 +318,9 @@ class RelayerSpec extends TestkitBaseClass { sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, InsufficientFunds(channelId_bc, origin.amountMsatOut, 100, 0, 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(flags = "0002") + val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = 2) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ChannelUnavailable(channelId_bc), origin, Some(channelUpdate_bc_disabled), None))) - assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ChannelDisabled(channelUpdate_bc_disabled.flags, channelUpdate_bc_disabled))) + assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ChannelDisabled(channelUpdate_bc_disabled.messageFlags, channelUpdate_bc_disabled.channelFlags, channelUpdate_bc_disabled))) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcTimedout(channelId_bc), origin, None, None))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(PermanentChannelFailure)) 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 b3b2a8ffa..9e96a810d 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 @@ -101,6 +101,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) + Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, c.node1Key, c.node2Key.publicKey, shortChannelId, 10, 1000, 10, 100, 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 998b69d58..77bd6b99b 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 @@ -53,7 +53,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) + 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) assert(checkSig(ann, Alice.nodeParams.nodeId)) assert(checkSig(ann, randomKey.publicKey) === false) } @@ -64,22 +64,22 @@ class AnnouncementsSpec extends FunSuite { // NB: node1 < node2 (public keys) assert(isNode1(node1_priv.publicKey.toBin, node2_priv.publicKey.toBin)) assert(!isNode1(node2_priv.publicKey.toBin, node1_priv.publicKey.toBin)) - val channelUpdate1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, enable = true) - val channelUpdate1_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, enable = false) - val channelUpdate2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, enable = true) - val channelUpdate2_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, enable = false) - assert(channelUpdate1.flags == BinaryData("0000")) // ....00 - assert(channelUpdate1_disabled.flags == BinaryData("0002")) // ....10 - assert(channelUpdate2.flags == BinaryData("0001")) // ....01 - assert(channelUpdate2_disabled.flags == BinaryData("0003")) // ....11 - assert(isNode1(channelUpdate1.flags)) - assert(isNode1(channelUpdate1_disabled.flags)) - assert(!isNode1(channelUpdate2.flags)) - assert(!isNode1(channelUpdate2_disabled.flags)) - assert(isEnabled(channelUpdate1.flags)) - assert(!isEnabled(channelUpdate1_disabled.flags)) - assert(isEnabled(channelUpdate2.flags)) - assert(!isEnabled(channelUpdate2_disabled.flags)) + 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) + assert(channelUpdate1.channelFlags == 0) // ....00 + assert(channelUpdate1_disabled.channelFlags == 2) // ....10 + assert(channelUpdate2.channelFlags == 1) // ....01 + assert(channelUpdate2_disabled.channelFlags == 3) // ....11 + assert(isNode1(channelUpdate1.channelFlags)) + assert(isNode1(channelUpdate1_disabled.channelFlags)) + assert(!isNode1(channelUpdate2.channelFlags)) + assert(!isNode1(channelUpdate2_disabled.channelFlags)) + assert(isEnabled(channelUpdate1.channelFlags)) + assert(!isEnabled(channelUpdate1_disabled.channelFlags)) + assert(isEnabled(channelUpdate2.channelFlags)) + assert(!isEnabled(channelUpdate2_disabled.channelFlags)) } } 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 bbe2c2ea0..3b25d5e30 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 @@ -75,14 +75,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, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10) - val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10) - val channelUpdate_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 5, 0, feeBaseMsat = 233000, feeProportionalMillionths = 1) - val channelUpdate_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, cltvExpiryDelta = 5, 0, feeBaseMsat = 233000, feeProportionalMillionths = 1) - val channelUpdate_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, 0, feeBaseMsat = 153000, feeProportionalMillionths = 4) - val channelUpdate_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, cltvExpiryDelta = 3, 0, feeBaseMsat = 153000, feeProportionalMillionths = 4) - val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, cltvExpiryDelta = 9, 0, feeBaseMsat = 786000, feeProportionalMillionths = 8) - val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, cltvExpiryDelta = 9, 0, feeBaseMsat = 786000, feeProportionalMillionths = 8) + val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L) + val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L) + val channelUpdate_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = 0, feeBaseMsat = 233000, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000L) + val channelUpdate_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = 0, feeBaseMsat = 233000, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000L) + val channelUpdate_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = 0, feeBaseMsat = 153000, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L) + val channelUpdate_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = 0, feeBaseMsat = 153000, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L) + val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 786000, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L) + val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 786000, feeProportionalMillionths = 8, htlcMaximumMsat = 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/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 865d466eb..0cf6f43db 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 @@ -188,14 +188,14 @@ class RouteCalculationSpec extends FunSuite { val DUMMY_SIG = BinaryData("3045022100e0a180fdd0fe38037cc878c03832861b40a29d32bd7b40b10c9e1efc8c1468a002205ae06d1624896d0d29f4b31e32772ea3cb1b4d7ed4e077e5da28dcc33c0e781201") - val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 0L, "0000", 1, 42, 2500, 140) - val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 1L, "0001", 1, 43, 2501, 141) - val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, "0000", 1, 44, 2502, 142) - val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, "0001", 1, 45, 2503, 143) - val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, "0000", 1, 46, 2504, 144) - val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, "0001", 1, 47, 2505, 145) - val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, "0000", 1, 48, 2506, 146) - val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, "0001", 1, 49, 2507, 147) + 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 updates = Map( ChannelDesc(ShortChannelId(1L), a, b) -> uab, @@ -336,7 +336,7 @@ object RouteCalculationSpec { } def makeUpdate(shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, feeBaseMsat: Int, feeProportionalMillionth: Int): (ChannelDesc, ChannelUpdate) = - (ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2) -> ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), 0L, "0000", 1, 42, feeBaseMsat, feeProportionalMillionth)) + (ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2) -> ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), 0L, 0, 0, 1, 42, feeBaseMsat, feeProportionalMillionth, None)) def makeGraph(updates: Map[ChannelDesc, ChannelUpdate]) = { 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 edcf287ae..d18c69ce3 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 @@ -47,21 +47,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) + val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId_ac, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 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) + val update_ax = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_x.publicKey, chan_ax.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 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) + val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 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) + val update_az = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_z.publicKey, chan_az.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) router ! PeerRoutingMessage(null, remoteNodeId, chan_ac) router ! PeerRoutingMessage(null, remoteNodeId, chan_ax) @@ -196,7 +196,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, enable = false) + val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, 0, feeBaseMsat = 153000, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L, enable = false) sender.send(router, PeerRoutingMessage(null, remoteNodeId, channelUpdate_cd1)) sender.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1)) sender.send(router, RouteRequest(a, d)) @@ -253,7 +253,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 / 1000 - 1209600 - 1 - val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, timestamp = timestamp) + val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 5, timestamp = timestamp) val probe = TestProbe() probe.ignoreMsg { case _: TransportHandler.ReadAck => true } probe.send(router, PeerRoutingMessage(null, remoteNodeId, announcement)) @@ -267,7 +267,7 @@ class RouterSpec extends BaseRouterSpec { val state = sender.expectMsgType[RoutingState] - val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, timestamp = Platform.currentTime / 1000) + val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L, timestamp = Platform.currentTime / 1000) // 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 f21f3b963..ff5c861dc 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 @@ -116,8 +116,8 @@ object RoutingSyncSpec { 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 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 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) 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 6e6c7ebb7..ecc4c0ba8 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 @@ -34,10 +34,12 @@ class FailureMessageCodecsSpec extends FunSuite { shortChannelId = ShortChannelId(12345), timestamp = 1234567L, cltvExpiryDelta = 100, - flags = BinaryData("0001"), + messageFlags = 0, + channelFlags = 1, htlcMinimumMsat = 1000, feeBaseMsat = 12, - feeProportionalMillionths = 76) + feeProportionalMillionths = 76, + htlcMaximumMsat = None) def randomBytes(size: Int): BinaryData = { val bin = new Array[Byte](size) @@ -51,7 +53,7 @@ class FailureMessageCodecsSpec extends FunSuite { InvalidOnionVersion(randomBytes(32)) :: InvalidOnionHmac(randomBytes(32)) :: InvalidOnionKey(randomBytes(32)) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: AmountBelowMinimum(123456, channelUpdate) :: FeeInsufficient(546463, channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: - UnknownPaymentHash :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(BinaryData("0101"), channelUpdate) :: ExpiryTooFar :: Nil + UnknownPaymentHash :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil msgs.foreach { case msg => { @@ -65,7 +67,7 @@ class FailureMessageCodecsSpec extends FunSuite { test("support encoding of channel_update with/without type in failure messages") { val tmp_channel_failure_notype = BinaryData("10070080cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001") val tmp_channel_failure_withtype = BinaryData("100700820102cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001") - val ref = TemporaryChannelFailure(ChannelUpdate(BinaryData("3045022100cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b022008e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f457821901"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, BinaryData("0003"), 14, 1000, 1, 1)) + val ref = TemporaryChannelFailure(ChannelUpdate(BinaryData("3045022100cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b022008e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f457821901"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, 0, 3, 14, 1000, 1, 1, None)) val u = FailureMessageCodecs.failureMessageCodec.decode(BitVector.apply(tmp_channel_failure_notype.data)).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 fb32f78d9..27999f1fa 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,9 +19,10 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress} import com.google.common.net.InetAddresses -import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar} +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.wire.LightningMessageCodecs._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomBytes, randomKey} import org.scalatest.FunSuite @@ -191,7 +192,7 @@ class LightningMessageCodecsSpec extends FunSuite { //BinaryData("a483677744b63d892a85fb7460fd6cb0504f802600956eb18cfaad05fbbe775328e4a7060476d2c0f3b7a6d505bb4de9377a55b27d1477baf14c367287c3de7900005abb440002dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607"), //BinaryData("3ecfd85bcb3bafb5bad14ab7f6323a2df33e161c37c2897e576762fa90ffe46078d231ebbf7dce3eff4b440d091a10ea9d092e698a321bb9c6b30869e2782c9900005abbebe202dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607"), //BinaryData("ad40baf5c7151777cc8896bc70ad2d0fd2afff47f4befb3883a78911b781a829441382d82625b77a47b9c2c71d201aab7187a6dc80e7d2d036dcb1186bac273c00005abffc330341f5ff2992997613aff5675d6796232a63ab7f30136219774da8aba431df37c80341f563377a6763723364776d777a7a3261652e6f6e696f6e00000000000000000000000f0317f2614763b32d9ce804fc002607") - ) + ) anns.foreach { ann => val bin = ByteVector(ann.data.toArray).toBitVector @@ -219,7 +220,7 @@ class LightningMessageCodecsSpec extends FunSuite { val revoke_and_ack = RevokeAndAck(randomBytes(32), scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomSignature, randomSignature, randomSignature, randomSignature, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) val node_announcement = NodeAnnouncement(randomSignature, bin(0, 0), 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(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, bin(2, 2), 3, 4, 5, 6) + 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)) @@ -265,12 +266,13 @@ class LightningMessageCodecsSpec extends FunSuite { val revoke_and_ack = RevokeAndAck(randomBytes(32), scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomSignature, randomSignature, randomSignature, randomSignature, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) val node_announcement = NodeAnnouncement(randomSignature, bin(0, 0), 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(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, bin(2, 2), 3, 4, 5, 6) + val channel_update1 = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 1, 0, 3, 4, 5, 6, Some(50000000L)) + val channel_update2 = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 0, 0, 3, 4, 5, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes(32), ShortChannelId(42), randomSignature, randomSignature) val ping = Ping(100, BinaryData("01" * 10)) val pong = Pong(BinaryData("01" * 10)) - val cached = channel_announcement :: node_announcement :: channel_update :: Nil + val cached = channel_announcement :: node_announcement :: channel_update1 :: channel_update2 :: Nil val nonCached = commit_sig :: revoke_and_ack :: announcement_signatures :: ping :: pong :: Nil val msgs: List[LightningMessage] = cached ::: nonCached @@ -289,6 +291,17 @@ class LightningMessageCodecsSpec extends FunSuite { } + test("decode channel_update with htlc_maximum_msat") { + // this was generated by c-lightning + val bin = BinaryData("010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00") + val update = LightningMessageCodecs.lightningMessageCodec.decode(BitVector(bin.toArray)).require.value.asInstanceOf[ChannelUpdate] + assert(update === ChannelUpdate("3044022058fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b0220634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792301", "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", ShortChannelId(0x5a10000020000L), 1539791129, 1, 1, 6, 1, 1, 10, Some(980000000L))) + val nodeId = PublicKey("03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") + assert(Announcements.checkSig(update, nodeId)) + val bin2 = BinaryData(LightningMessageCodecs.lightningMessageCodec.encode(update).require.toByteArray) + assert(bin === bin2) + } + } object LightningMessageCodecsSpec { 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 da95dabfa..91a1eac88 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 @@ -193,17 +193,17 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging } case ChannelUpdateReceived(channelUpdate) => - log.debug(s"peer channel with id=${channelUpdate.shortChannelId} has been updated - flags: ${channelUpdate.flags} fees: ${channelUpdate.feeBaseMsat} ${channelUpdate.feeProportionalMillionths}") + log.debug(s"peer channel with id=${channelUpdate.shortChannelId} has been updated - flags: ${channelUpdate.channelFlags} fees: ${channelUpdate.feeBaseMsat} ${channelUpdate.feeProportionalMillionths}") runInGuiThread { () => val idx = mainController.networkChannelsList.indexWhere(c => c.announcement.shortChannelId == channelUpdate.shortChannelId) if (idx >= 0) { val c = mainController.networkChannelsList.get(idx) - if (Announcements.isNode1(channelUpdate.flags)) { - c.isNode1Enabled = Some(Announcements.isEnabled(channelUpdate.flags)) + if (Announcements.isNode1(channelUpdate.channelFlags)) { + c.isNode1Enabled = Some(Announcements.isEnabled(channelUpdate.channelFlags)) c.feeBaseMsatNode1_opt = Some(channelUpdate.feeBaseMsat) c.feeProportionalMillionthsNode1_opt = Some(channelUpdate.feeProportionalMillionths) } else { - c.isNode2Enabled = Some(Announcements.isEnabled(channelUpdate.flags)) + c.isNode2Enabled = Some(Announcements.isEnabled(channelUpdate.channelFlags)) c.feeBaseMsatNode2_opt = Some(channelUpdate.feeBaseMsat) c.feeProportionalMillionthsNode2_opt = Some(channelUpdate.feeProportionalMillionths) } From 6126ed4b9e31e0be3162525ab48c60f962455d5d Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Fri, 19 Oct 2018 17:58:20 +0200 Subject: [PATCH 21/31] Fix encoding of FinalIncorrectHtlcAmount error message (#740) --- .../src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 77e7af173..0fff5385b 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 @@ -92,6 +92,6 @@ object FailureMessageCodecs { .typecase(PERM | 16, provide(IncorrectPaymentAmount)) .typecase(17, provide(FinalExpiryTooSoon)) .typecase(18, (("expiry" | uint32)).as[FinalIncorrectCltvExpiry]) - .typecase(19, (("amountMsat" | uint32)).as[FinalIncorrectHtlcAmount]) + .typecase(19, (("amountMsat" | uint64)).as[FinalIncorrectHtlcAmount]) .typecase(21, provide(ExpiryTooFar)) } From 52821b8664c329cc2e14e237df1aeff96b5260d5 Mon Sep 17 00:00:00 2001 From: sstone Date: Sat, 20 Oct 2018 21:34:34 +0200 Subject: [PATCH 22/31] set version to 0.2-beta8 --- eclair-core/pom.xml | 2 +- eclair-node-gui/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index a563e880b..3d320f186 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta8 eclair-core_2.11 diff --git a/eclair-node-gui/pom.xml b/eclair-node-gui/pom.xml index 293177cee..929199743 100644 --- a/eclair-node-gui/pom.xml +++ b/eclair-node-gui/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta8 eclair-node-gui_2.11 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 36c687f81..08f9831ea 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta8 eclair-node_2.11 diff --git a/pom.xml b/pom.xml index 26f18405c..0477b5ca1 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-SNAPSHOT + 0.2-beta8 pom From 4e0702a923afb691b5344cd0cb665c234106f1ea Mon Sep 17 00:00:00 2001 From: sstone Date: Sat, 20 Oct 2018 21:38:16 +0200 Subject: [PATCH 23/31] set version to 0.2-SNAPSHOT --- eclair-core/pom.xml | 2 +- eclair-node-gui/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 3d320f186..a563e880b 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta8 + 0.2-SNAPSHOT eclair-core_2.11 diff --git a/eclair-node-gui/pom.xml b/eclair-node-gui/pom.xml index 929199743..293177cee 100644 --- a/eclair-node-gui/pom.xml +++ b/eclair-node-gui/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta8 + 0.2-SNAPSHOT eclair-node-gui_2.11 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 08f9831ea..36c687f81 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta8 + 0.2-SNAPSHOT eclair-node_2.11 diff --git a/pom.xml b/pom.xml index 0477b5ca1..26f18405c 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-beta8 + 0.2-SNAPSHOT pom From 2bdf258c7827edf84ec8ab62ba8e8ff371ab365f Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 23 Oct 2018 17:31:22 +0200 Subject: [PATCH 24/31] Always add 1 block to the `finalCltvExpiry` (#742) This fixes #651. --- .../scala/fr/acinq/eclair/payment/PaymentLifecycle.scala | 6 +++--- .../test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 ce02a05bd..fa2ae0a45 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 @@ -51,7 +51,8 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Event(RouteResponse(hops, ignoreNodes, ignoreChannels), WaitingForRoute(s, c, failures)) => log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId).mkString("->")}") val firstHop = hops.head - val finalExpiry = Globals.blockCount.get().toInt + c.finalCltvExpiry.toInt + // 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(c.amountMsat, finalExpiry, c.paymentHash, hops) val feePct = (cmd.amountMsat - c.amountMsat) / c.amountMsat.toDouble // c.amountMsat is required to be > 0, have to convert to double, otherwise will be rounded @@ -190,10 +191,9 @@ object PaymentLifecycle { // @formatter:off case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: Seq[Seq[ExtraHop]] = Nil) /** - * @param finalCltvExpiry by default we choose finalCltvExpiry = Channel.MIN_CLTV_EXPIRY + 1 to not have our htlc fail when a new block has just been found * @param maxFeePct set by default to 3% as a safety measure (even if a route is found, if fee is higher than that payment won't be attempted) */ - case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY + 1, maxAttempts: Int = 5, maxFeePct: Double = 0.03) { + case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY, maxAttempts: Int = 5, maxFeePct: Double = 0.03) { require(amountMsat > 0, s"amountMsat must be > 0") } case class CheckPayment(paymentHash: BinaryData) 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 67847b1ac..4e220ce32 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 @@ -57,7 +57,7 @@ class SqliteAuditDbSpec extends FunSuite { db.add(e6) assert(db.listSent(from = 0L, to = Long.MaxValue).toSet === Set(e1, e5, e6)) - assert(db.listSent(from = 100000L, to = Platform.currentTime).toList === List(e1)) + assert(db.listSent(from = 100000L, to = Platform.currentTime + 1).toList === List(e1)) assert(db.listReceived(from = 0L, to = Long.MaxValue).toList === List(e2)) assert(db.listRelayed(from = 0L, to = Long.MaxValue).toList === List(e3)) assert(db.listNetworkFees(from = 0L, to = Long.MaxValue).size === 1) From 3a221e7e983672f44e34a8ba411b51ab1ada6974 Mon Sep 17 00:00:00 2001 From: Dominique <5765435+dpad85@users.noreply.github.com> Date: Tue, 23 Oct 2018 19:07:20 +0200 Subject: [PATCH 25/31] Support for custom Electrum server (#739) We can now use the `overrideDefaults` parameter in `Setup` to programmatically provide a custom electrum address when starting eclair, by setting the `eclair.electrum.host` and `eclair.electrum.port` entries in the configuration. When these entries are set, eclair-core will always try to connect to this server instead of relying on a random server picked from the preset lists. --- .../main/scala/fr/acinq/eclair/Setup.scala | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index f5a8be973..441e282eb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -46,12 +46,12 @@ import scala.concurrent.duration._ import scala.concurrent._ /** - * Setup eclair from a datadir. + * Setup eclair from a data directory. * * Created by PM on 25/01/2016. * - * @param datadir directory where eclair-core will write/read its data - * @param overrideDefaults + * @param datadir directory where eclair-core will write/read its data. + * @param overrideDefaults use this parameter to programmatically override the node configuration . * @param seed_opt optional seed, if set eclair will use it instead of generating one and won't create a seed.dat file. */ class Setup(datadir: File, @@ -122,13 +122,22 @@ class Setup(datadir: File, Bitcoind(bitcoinClient) case ELECTRUM => logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!") - val addressesFile = nodeParams.chainHash match { - case Block.RegtestGenesisBlock.hash => "/electrum/servers_regtest.json" - case Block.TestnetGenesisBlock.hash => "/electrum/servers_testnet.json" - case Block.LivenetGenesisBlock.hash => "/electrum/servers_mainnet.json" + val addresses = config.hasPath("eclair.electrum") match { + case true => + val host = config.getString("eclair.electrum.host") + val port = config.getInt("eclair.electrum.port") + val address = InetSocketAddress.createUnresolved(host, port) + logger.info(s"override electrum default with server=$address") + Set(address) + case false => + val addressesFile = nodeParams.chainHash match { + case Block.RegtestGenesisBlock.hash => "/electrum/servers_regtest.json" + case Block.TestnetGenesisBlock.hash => "/electrum/servers_testnet.json" + case Block.LivenetGenesisBlock.hash => "/electrum/servers_mainnet.json" + } + val stream = classOf[Setup].getResourceAsStream(addressesFile) + ElectrumClientPool.readServerAddresses(stream) } - val stream = classOf[Setup].getResourceAsStream(addressesFile) - val addresses = ElectrumClientPool.readServerAddresses(stream) val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClientPool(addresses)), "electrum-client", SupervisorStrategy.Resume)) Electrum(electrumClient) } From 5141cd7d4d6afb23c2fd01d4cab56f5c9622ee2f Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Thu, 25 Oct 2018 14:28:28 +0200 Subject: [PATCH 26/31] Use different ZMQ block and tx subscriptions Fixes #733 --- README.md | 3 ++- eclair-core/src/main/resources/reference.conf | 3 ++- .../src/main/scala/fr/acinq/eclair/Setup.scala | 15 ++++++++++----- .../src/test/resources/integration/bitcoin.conf | 2 +- .../eclair/integration/IntegrationSpec.scala | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 17d28b3e2..e46b9570f 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ name | description eclair.api.password | API password (BASIC) | "" (must be set if the API is enabled) eclair.bitcoind.rpcuser | Bitcoin Core RPC user | foo eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar - eclair.bitcoind.zmq | Bitcoin Core ZMQ address | "tcp://127.0.0.1:29000" + eclair.bitcoind.zmqblock | Bitcoin Core ZMQ block address | "tcp://127.0.0.1:29000" + eclair.bitcoind.zmqtx | Bitcoin Core ZMQ tx address | "tcp://127.0.0.1:29000" eclair.gui.unit | Unit in which amounts are displayed (possible values: msat, sat, mbtc, btc) | btc Quotes are not required unless the value contains special characters. Full syntax guide [here](https://github.com/lightbend/config/blob/master/HOCON.md). diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1930bf281..02412f53d 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -22,7 +22,8 @@ eclair { rpcport = 18332 rpcuser = "foo" rpcpassword = "bar" - zmq = "tcp://127.0.0.1:29000" + zmqblock = "tcp://127.0.0.1:29000" + zmqtx = "tcp://127.0.0.1:29000" } default-feerates { // those are in satoshis per kilobyte diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 441e282eb..dfc471fc5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -146,7 +146,8 @@ class Setup(datadir: File, for { _ <- Future.successful(true) feeratesRetrieved = Promise[Boolean]() - zmqConnected = Promise[Boolean]() + zmqBlockConnected = Promise[Boolean]() + zmqTxConnected = Promise[Boolean]() tcpBound = Promise[Unit]() defaultFeerates = FeeratesPerKB( @@ -178,10 +179,12 @@ class Setup(datadir: File, watcher = bitcoin match { case Bitcoind(bitcoinClient) => - system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart)) + system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart)) + system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart)) system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(new ExtendedBitcoinClient(new BatchingBitcoinJsonRPCClient(bitcoinClient))), "watcher", SupervisorStrategy.Resume)) case Electrum(electrumClient) => - zmqConnected.success(true) + zmqBlockConnected.success(true) + zmqTxConnected.success(true) system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume)) } @@ -221,10 +224,12 @@ class Setup(datadir: File, server = server, wallet = wallet) - zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException)) + zmqBlockTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException)) + zmqTxTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException)) tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port")))) - _ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil) + _ <- Future.firstCompletedOf(zmqBlockConnected.future :: zmqBlockTimeout :: Nil) + _ <- Future.firstCompletedOf(zmqTxConnected.future :: zmqTxTimeout :: Nil) _ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil) _ <- if (config.getBoolean("api.enabled")) { logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}") diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf index 25e7711fe..da4dd59a0 100644 --- a/eclair-core/src/test/resources/integration/bitcoin.conf +++ b/eclair-core/src/test/resources/integration/bitcoin.conf @@ -6,6 +6,6 @@ rpcuser=foo rpcpassword=bar txindex=1 zmqpubrawblock=tcp://127.0.0.1:28334 -zmqpubrawtx=tcp://127.0.0.1:28334 +zmqpubrawtx=tcp://127.0.0.1:28335 rpcworkqueue=64 addresstype=bech32 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 e4e0b317c..9e3c95bbc 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 @@ -108,7 +108,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("starting eclair nodes") { import collection.JavaConversions._ - val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "127.0.0.1", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.mindepth-blocks" -> 2, "eclair.max-htlc-value-in-flight-msat" -> 100000000000L, "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) + val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "127.0.0.1", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmqblock" -> "tcp://127.0.0.1:28334", "eclair.bitcoind.zmqtx" -> "tcp://127.0.0.1:28335", "eclair.mindepth-blocks" -> 2, "eclair.max-htlc-value-in-flight-msat" -> 100000000000L, "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.delay-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0)).withFallback(commonConfig)) // A's channels are private instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.delay-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig)) instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.delay-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) From 2de7c6b07d558b614cabf8c77f77d7aab5f607e4 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 25 Oct 2018 16:45:27 +0200 Subject: [PATCH 27/31] Use sttp lib instead of akka-http-client (#720) Also updated json4s-jackson to 3.6.0 --- eclair-core/pom.xml | 14 ++++- .../main/scala/fr/acinq/eclair/Setup.scala | 9 ++- .../bitcoind/BitcoinCoreWallet.scala | 4 +- .../rpc/BasicBitcoinJsonRPCClient.scala | 61 +++++-------------- .../fee/BitcoinCoreFeeProvider.scala | 2 +- .../blockchain/fee/BitgoFeeProvider.scala | 28 ++++----- .../fee/EarnDotComFeeProvider.scala | 26 ++++---- .../fr/acinq/eclair/TestBitcoinClient.scala | 2 +- .../bitcoind/BitcoinCoreWalletSpec.scala | 1 - .../blockchain/bitcoind/BitcoindService.scala | 2 + .../electrum/ElectrumWalletSpec.scala | 1 + .../fee/EarnDotComFeeProviderSpec.scala | 4 +- .../eclair/integration/IntegrationSpec.scala | 3 +- .../AnnouncementsBatchValidationSpec.scala | 4 +- pom.xml | 1 + 15 files changed, 71 insertions(+), 91 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index a563e880b..b842b4624 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -126,22 +126,34 @@ akka-slf4j_${scala.version.short} ${akka.version} + com.typesafe.akka akka-http-core_${scala.version.short} 10.0.11 + + + com.softwaremill.sttp + async-http-client-backend-future_${scala.version.short} + ${sttp.version} + org.json4s json4s-jackson_${scala.version.short} - 3.5.3 + 3.6.0 de.heikoseeberger akka-http-json4s_${scala.version.short} 1.19.0 + + com.softwaremill.sttp + json4s_${scala.version.short} + ${sttp.version} + fr.acinq diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index dfc471fc5..ea6b24c9f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -24,6 +24,7 @@ import akka.http.scaladsl.Http import akka.pattern.after import akka.stream.{ActorMaterializer, BindFailedException} import akka.util.Timeout +import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} @@ -81,10 +82,8 @@ class Setup(datadir: File, // this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala) secureRandom.nextInt() - implicit val materializer = ActorMaterializer() - implicit val timeout = Timeout(30 seconds) - implicit val formats = org.json4s.DefaultFormats implicit val ec = ExecutionContext.Implicits.global + implicit val sttpBackend = AsyncHttpClientFutureBackend() val bitcoin = nodeParams.watcherType match { case BITCOIND => @@ -93,6 +92,8 @@ class Setup(datadir: File, password = config.getString("bitcoind.rpcpassword"), host = config.getString("bitcoind.host"), port = config.getInt("bitcoind.rpcport")) + implicit val timeout = Timeout(30 seconds) + implicit val formats = org.json4s.DefaultFormats val future = for { json <- bitcoinClient.invoke("getblockchaininfo").recover { case _ => throw BitcoinRPCConnectionException } // Make sure wallet support is enabled in bitcoind. @@ -192,6 +193,7 @@ class Setup(datadir: File, case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient) case Electrum(electrumClient) => val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash)), "electrum-wallet") + implicit val timeout = Timeout(30 seconds) new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash) } _ = wallet.getFinalAddress.map { @@ -233,6 +235,7 @@ class Setup(datadir: File, _ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil) _ <- if (config.getBoolean("api.enabled")) { logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}") + implicit val materializer = ActorMaterializer() val api = new Service { override def scheduler = system.scheduler diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index 61c34693e..d608e92c8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.blockchain.bitcoind -import akka.actor.ActorSystem import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ @@ -30,11 +29,10 @@ import scala.concurrent.{ExecutionContext, Future} /** * Created by PM on 06/07/2017. */ -class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging { +class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionContext) extends EclairWallet with Logging { import BitcoinCoreWallet._ - def fundTransaction(hex: String, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = { val feeRatePerKB = BigDecimal(feerateKw2KB(feeRatePerKw)) rpcClient.invoke("fundrawtransaction", hex, Options(lockUnspents, feeRatePerKB.bigDecimal.scaleByPowerOfTen(-8))).map(json => { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala index e90c1c68b..5be0d2a82 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala @@ -16,52 +16,19 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import akka.actor.ActorSystem -import akka.event.Logging -import akka.http.scaladsl.Http -import akka.http.scaladsl.marshalling.Marshal -import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.scaladsl.{Keep, Sink, Source} -import akka.stream.{ActorMaterializer, OverflowStrategy, QueueOfferResult} -import de.heikoseeberger.akkahttpjson4s.Json4sSupport._ +import com.softwaremill.sttp._ +import com.softwaremill.sttp.json4s._ +import org.json4s.DefaultFormats import org.json4s.JsonAST.JValue -import org.json4s.{DefaultFormats, jackson} +import org.json4s.jackson.Serialization -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.util.{Failure, Success} +import scala.concurrent.{ExecutionContext, Future} -class BasicBitcoinJsonRPCClient(user: String, password: String, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false)(implicit system: ActorSystem) extends BitcoinJsonRPCClient { +class BasicBitcoinJsonRPCClient(user: String, password: String, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false)(implicit http: SttpBackend[Future, Nothing]) extends BitcoinJsonRPCClient { val scheme = if (ssl) "https" else "http" - val uri = Uri(s"$scheme://$host:$port") - implicit val serialization = jackson.Serialization implicit val formats = DefaultFormats.withBigDecimal - val log = Logging(system, classOf[BasicBitcoinJsonRPCClient]) - - implicit val materializer = ActorMaterializer() - val httpClientFlow = Http().cachedHostConnectionPool[Promise[HttpResponse]](host, port) - - val queueSize = 256 - val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](queueSize, OverflowStrategy.dropNew) - .via(httpClientFlow) - .toMat(Sink.foreach({ - case ((Success(resp), p)) => p.success(resp) - case ((Failure(e), p)) => p.failure(e) - }))(Keep.left) - .run() - - def queueRequest(request: HttpRequest): Future[HttpResponse] = { - val responsePromise = Promise[HttpResponse]() - queue.offer(request -> responsePromise).flatMap { - case QueueOfferResult.Enqueued => responsePromise.future - case QueueOfferResult.Dropped => Future.failed(new RuntimeException("Queue overflowed. Try again later.")) - case QueueOfferResult.Failure(ex) => Future.failed(ex) - case QueueOfferResult.QueueClosed => Future.failed(new RuntimeException("Queue was closed (pool shut down) while running the request. Try again later.")) - } - } + implicit val serialization = Serialization override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] = invoke(Seq(JsonRPCRequest(method = method, params = params))).map(l => jsonResponse2Exception(l.head).result) @@ -73,12 +40,12 @@ class BasicBitcoinJsonRPCClient(user: String, password: String, host: String = " def invoke(requests: Seq[JsonRPCRequest])(implicit ec: ExecutionContext): Future[Seq[JsonRPCResponse]] = for { - entity <- Marshal(requests).to[RequestEntity] - _ = log.debug("sending rpc request with body={}", entity) - httpRes <- queueRequest(HttpRequest(uri = "/", method = HttpMethods.POST).addHeader(Authorization(BasicHttpCredentials(user, password))).withEntity(entity)) - jsonRpcRes <- Unmarshal(httpRes).to[Seq[JsonRPCResponse]].recover { - case t: Throwable if httpRes.status == StatusCodes.Unauthorized => throw new RuntimeException("bitcoind replied with 401/Unauthorized (bad user/password?)", t) - } - } yield jsonRpcRes + res <- sttp + .post(uri"$scheme://$host:$port") + .body(requests) + .auth.basic(user, password) + .response(asJson[Seq[JsonRPCResponse]]) + .send() + } yield res.unsafeBody } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala index 5cc6e7ae7..b56c878df 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.blockchain.fee import fr.acinq.bitcoin._ -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, Error, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient import org.json4s.DefaultFormats import org.json4s.JsonAST._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala index 03861ffc1..bf6815186 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala @@ -16,37 +16,33 @@ package fr.acinq.eclair.blockchain.fee -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.model._ -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.ActorMaterializer -import de.heikoseeberger.akkahttpjson4s.Json4sSupport._ +import com.softwaremill.sttp._ +import com.softwaremill.sttp.json4s._ import fr.acinq.bitcoin.{BinaryData, Block} +import org.json4s.DefaultFormats import org.json4s.JsonAST.{JInt, JValue} -import org.json4s.{DefaultFormats, jackson} +import org.json4s.jackson.Serialization import scala.concurrent.{ExecutionContext, Future} -class BitgoFeeProvider(chainHash: BinaryData)(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider { +class BitgoFeeProvider(chainHash: BinaryData)(implicit http: SttpBackend[Future, Nothing], ec: ExecutionContext) extends FeeProvider { import BitgoFeeProvider._ - implicit val materializer = ActorMaterializer() - val httpClient = Http(system) - implicit val serialization = jackson.Serialization implicit val formats = DefaultFormats + implicit val serialization = Serialization val uri = chainHash match { - case Block.LivenetGenesisBlock.hash => Uri("https://www.bitgo.com/api/v2/btc/tx/fee") - case _ => Uri("https://test.bitgo.com/api/v2/tbtc/tx/fee") + case Block.LivenetGenesisBlock.hash => uri"https://www.bitgo.com/api/v2/btc/tx/fee" + case _ => uri"https://test.bitgo.com/api/v2/tbtc/tx/fee" } override def getFeerates: Future[FeeratesPerKB] = for { - httpRes <- httpClient.singleRequest(HttpRequest(uri = uri, method = HttpMethods.GET)) - json <- Unmarshal(httpRes).to[JValue] - feeRanges = parseFeeRanges(json) + res <- sttp.get(uri) + .response(asJson[JValue]) + .send() + feeRanges = parseFeeRanges(res.unsafeBody) } yield extractFeerates(feeRanges) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala index 6919c7a14..2176ce481 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala @@ -16,34 +16,32 @@ package fr.acinq.eclair.blockchain.fee -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.model._ -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.ActorMaterializer -import de.heikoseeberger.akkahttpjson4s.Json4sSupport._ +import com.softwaremill.sttp._ +import com.softwaremill.sttp.json4s._ +import org.json4s.DefaultFormats import org.json4s.JsonAST.{JArray, JInt, JValue} -import org.json4s.{DefaultFormats, jackson} +import org.json4s.jackson.Serialization import scala.concurrent.{ExecutionContext, Future} /** * Created by PM on 16/11/2017. */ -class EarnDotComFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider { +class EarnDotComFeeProvider(implicit http: SttpBackend[Future, Nothing], ec: ExecutionContext) extends FeeProvider { import EarnDotComFeeProvider._ - implicit val materializer = ActorMaterializer() - val httpClient = Http(system) - implicit val serialization = jackson.Serialization implicit val formats = DefaultFormats + implicit val serialization = Serialization + + val uri = uri"https://bitcoinfees.earn.com/api/v1/fees/list" override def getFeerates: Future[FeeratesPerKB] = for { - httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://bitcoinfees.earn.com/api/v1/fees/list"), method = HttpMethods.GET)) - json <- Unmarshal(httpRes).to[JValue] - feeRanges = parseFeeRanges(json) + json <- sttp.get(uri) + .response(asJson[JValue]) + .send() + feeRanges = parseFeeRanges(json.unsafeBody) } yield extractFeerates(feeRanges) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala index 29eda1873..3d0d16156 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinClient.scala @@ -27,7 +27,7 @@ import scala.concurrent.{ExecutionContext, Future} /** * Created by PM on 26/04/2016. */ -class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinClient(new BasicBitcoinJsonRPCClient("", "", "", 0)) { +class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinClient(new BasicBitcoinJsonRPCClient("", "", "", 0)(http = null)) { import scala.concurrent.ExecutionContext.Implicits.global 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 e0a648478..4acb1255d 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 @@ -48,7 +48,6 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe implicit val formats = DefaultFormats - override def beforeAll(): Unit = { startBitcoind() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 67a171abb..edabdebd6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -23,6 +23,7 @@ import java.util.UUID import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.pattern.pipe import akka.testkit.{TestKitBase, TestProbe} +import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient} import fr.acinq.eclair.integration.IntegrationSpec import grizzled.slf4j.Logging @@ -35,6 +36,7 @@ trait BitcoindService extends Logging { self: TestKitBase => implicit val system: ActorSystem + implicit val sttpBackend = AsyncHttpClientFutureBackend() import scala.sys.process._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala index 02b286256..e423c1899 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala @@ -28,6 +28,7 @@ import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse} import grizzled.slf4j.Logging import org.json4s.JsonAST.{JDecimal, JString, JValue} +import org.scalatest.junit.JUnitRunner import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.concurrent.Await diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala index a0c79750e..d8197bc95 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.blockchain.fee -import akka.actor.ActorSystem import akka.util.Timeout +import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend import grizzled.slf4j.Logging import org.json4s.DefaultFormats import org.scalatest.FunSuite @@ -70,7 +70,7 @@ class EarnDotComFeeProviderSpec extends FunSuite with Logging { test("make sure API hasn't changed") { import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ - implicit val system = ActorSystem() + implicit val sttpBackend = AsyncHttpClientFutureBackend() implicit val timeout = Timeout(30 seconds) val provider = new EarnDotComFeeProvider() logger.info("earn.com livenet fees: " + Await.result(provider.getFeerates, 10 seconds)) 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 9e3c95bbc..9ac91adfd 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 @@ -19,7 +19,8 @@ package fr.acinq.eclair.integration import java.io.{File, PrintWriter} import java.util.Properties -import akka.actor.{ActorRef, ActorSystem} +import akka.actor.{ActorRef, ActorSystem, Terminated} +import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.google.common.net.HostAndPort import com.typesafe.config.{Config, ConfigFactory} 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 9e96a810d..de0986e5d 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 @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.pattern.pipe import akka.testkit.TestProbe +import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction} import fr.acinq.eclair.blockchain.ValidateResult @@ -44,6 +45,7 @@ class AnnouncementsBatchValidationSpec extends FunSuite { import scala.concurrent.ExecutionContext.Implicits.global implicit val system = ActorSystem() + implicit val sttpBackend = AsyncHttpClientFutureBackend() implicit val extendedBitcoinClient = new ExtendedBitcoinClient(new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 18332)) val channels = for (i <- 0 until 50) yield { @@ -76,7 +78,7 @@ object AnnouncementsBatchValidationSpec { def generateBlocks(numBlocks: Int)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext) = Await.result(extendedBitcoinClient.rpcClient.invoke("generate", numBlocks), 10 seconds) - def simulateChannel()(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext, system: ActorSystem): SimulatedChannel = { + def simulateChannel()(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): SimulatedChannel = { val node1Key = randomKey val node2Key = randomKey val node1BitcoinKey = randomKey diff --git a/pom.xml b/pom.xml index 26f18405c..e82319b2c 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,7 @@ 2.11.11 2.11 2.4.20 + 1.3.9 0.9.17 24.0-android From 93464aa7fb4f46b4375c47a06824dbd05522bb3e Mon Sep 17 00:00:00 2001 From: Jens Schendel <30713344+jagottsicher@users.noreply.github.com> Date: Wed, 14 Nov 2018 00:35:16 +0800 Subject: [PATCH 28/31] Updated readme.md and eclair-cli (#750) * Minor changes. Typos and grammar corrected. * Added check for curl and hint ift not installed --- README.md | 4 ++-- eclair-core/eclair-cli | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e46b9570f..ae2e5fbad 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) [![Gitter chat](https://img.shields.io/badge/chat-on%20gitter-red.svg)](https://gitter.im/ACINQ/eclair) -**Eclair** (french for Lightning) is a scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON-RPC API is also available. +**Eclair** (French for Lightning) is a Scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON-RPC 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). @@ -209,7 +209,7 @@ addresstype=p2sh-segwit deprecatedrpc=signrawtransaction ``` -You may also want to take advantage of the new configuration sections in `bitcoin.conf` to manage parameters that are network speficic, so you can reasliy run your bitcoin node on both mainnet and testnet. For example you could use: +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: ``` server=1 diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 013346957..66e9cd358 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -2,6 +2,9 @@ # Check if jq is installed. If not, display instructions and abort program command -v jq >/dev/null 2>&1 || { echo -e "This tool requires jq.\nFor installation instructions, visit https://stedolan.github.io/jq/download/.\n\nAborting..."; exit 1; } +# curl installed? If not, give a hint +command -v curl >/dev/null 2>&1 || { echo -e "This tool requires curl.\n\nAborting..."; exit 1; } + FULL_OUTPUT='false' URL='http://localhost:8080' PASSWORD='' From 738b4fa8e90353509cd75293d673e08545b34923 Mon Sep 17 00:00:00 2001 From: n1bor Date: Thu, 15 Nov 2018 13:08:27 +0000 Subject: [PATCH 29/31] Reject received payments where PaymentRequest Expiry Tag time has been exceeded. (#749) This fixes #748. Also renamed htlc `expiry` to `cltvExpiry`. This removes confusion between different types of expiries and is how the spec is written. --- .../fr/acinq/eclair/channel/Channel.scala | 4 +- .../acinq/eclair/channel/ChannelTypes.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 22 ++++----- .../eclair/payment/LocalPaymentHandler.scala | 45 ++++++++++++++----- .../fr/acinq/eclair/payment/Relayer.scala | 8 ++-- .../eclair/transactions/Transactions.scala | 10 ++--- .../eclair/wire/LightningMessageTypes.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 18 ++++---- .../channel/states/f/ShutdownStateSpec.scala | 2 +- .../states/g/NegotiatingStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../eclair/payment/HtlcGenerationSpec.scala | 4 +- .../eclair/payment/PaymentHandlerSpec.scala | 38 +++++++++++++--- .../fr/acinq/eclair/payment/RelayerSpec.scala | 32 ++++++------- .../eclair/transactions/TestVectorsSpec.scala | 4 +- .../transactions/TransactionsSpec.scala | 12 ++--- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 6 +-- 17 files changed, 129 insertions(+), 84 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 5ea1b5df2..5845c9105 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 @@ -617,8 +617,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu val trimmedHtlcs = Transactions.trimOfferedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) ++ Transactions.trimReceivedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) trimmedHtlcs collect { case DirectedHtlc(_, u) => - log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.expiry} to htlcs db for commitNumber=$nextCommitNumber") - nodeParams.channelsDb.addOrUpdateHtlcInfo(d.channelId, nextCommitNumber, u.paymentHash, u.expiry) + log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") + nodeParams.channelsDb.addOrUpdateHtlcInfo(d.channelId, nextCommitNumber, u.paymentHash, u.cltvExpiry) } context.system.eventStream.publish(ChannelSignatureSent(self, commitments1)) context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, nextRemoteCommit.spec.toRemoteMsat)) // note that remoteCommit.toRemote == toLocal 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 2bbc8d139..d3ea2e2f8 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 @@ -105,7 +105,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, expiry: Long, onion: BinaryData = Sphinx.LAST_PACKET.serialize, upstream_opt: Option[UpdateAddHtlc] = None, commit: Boolean = false, redirected: Boolean = false) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, cltvExpiry: Long, onion: BinaryData = Sphinx.LAST_PACKET.serialize, upstream_opt: Option[UpdateAddHtlc] = None, commit: Boolean = false, redirected: Boolean = false) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: BinaryData, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[BinaryData, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: BinaryData, failureCode: Int, commit: Boolean = false) extends Command 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 b8704aee4..e1afe7cc5 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 @@ -62,9 +62,9 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams, def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight def hasTimedoutOutgoingHtlcs(blockheight: Long): Boolean = - localCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && blockheight >= htlc.add.expiry) || - remoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.expiry) || - remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.expiry)).getOrElse(false) + localCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && blockheight >= htlc.add.cltvExpiry) || + remoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry) || + remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry)).getOrElse(false) def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal) @@ -102,13 +102,13 @@ object Commitments { val blockCount = Globals.blockCount.get() // our counterparty needs a reasonable amount of time to pull the funds from downstream before we can get refunded (see BOLT 2 and BOLT 11 for a calculation and rationale) val minExpiry = blockCount + Channel.MIN_CLTV_EXPIRY - if (cmd.expiry < minExpiry) { - return Left(ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = cmd.expiry, blockCount = blockCount)) + if (cmd.cltvExpiry < minExpiry) { + return Left(ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = cmd.cltvExpiry, blockCount = blockCount)) } val maxExpiry = blockCount + Channel.MAX_CLTV_EXPIRY // we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled - if (cmd.expiry >= maxExpiry) { - return Left(ExpiryTooBig(commitments.channelId, maximum = maxExpiry, actual = cmd.expiry, blockCount = blockCount)) + if (cmd.cltvExpiry >= maxExpiry) { + return Left(ExpiryTooBig(commitments.channelId, maximum = maxExpiry, actual = cmd.cltvExpiry, blockCount = blockCount)) } if (cmd.amountMsat < commitments.remoteParams.htlcMinimumMsat) { @@ -116,7 +116,7 @@ object Commitments { } // 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.expiry, cmd.onion) + val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amountMsat, 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 @@ -540,17 +540,17 @@ object Commitments { | toLocal: ${commitments.localCommit.spec.toLocalMsat} | toRemote: ${commitments.localCommit.spec.toRemoteMsat} | htlcs: - |${commitments.localCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")} + |${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} | htlcs: - |${commitments.remoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")} + |${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")} | htlcs: - |${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin + |${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/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 2eef926c3..b75b6ee73 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 @@ -30,23 +30,25 @@ import scala.concurrent.duration._ import scala.util.Try /** + * Simple payment handler that generates payment requests and fulfills incoming htlcs. + * + * Note that unfulfilled payment requests are kept forever if they don't have an expiry! + * * Created by PM on 17/06/2016. */ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLogging { - implicit val ec: ExecutionContext = context.system.dispatcher + import LocalPaymentHandler._ - context.system.scheduler.schedule(10 minutes, 10 minutes)(self ! Platform.currentTime / 1000) + implicit val ec: ExecutionContext = context.system.dispatcher + context.system.scheduler.schedule(10 minutes, 10 minutes)(self ! PurgeExpiredRequests) override def receive: Receive = run(Map.empty) - def run(hash2preimage: Map[BinaryData, (BinaryData, PaymentRequest)]): Receive = { + def run(hash2preimage: Map[BinaryData, PendingPaymentRequest]): Receive = { - case currentSeconds: Long => - context.become(run(hash2preimage.collect { - case e@(_, (_, pr)) if pr.expiry.isEmpty => e // requests that don't expire are kept forever - case e@(_, (_, pr)) if pr.timestamp + pr.expiry.get > currentSeconds => e // clean up expired requests - })) + case PurgeExpiredRequests => + context.become(run(hash2preimage.filterNot { case (_, pr) => hasExpired(pr) })) case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops) => Try { @@ -59,7 +61,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress = None, expirySeconds = Some(expirySeconds), extraHops = extraHops) log.debug(s"generated payment request=${PaymentRequest.write(paymentRequest)} from amount=$amount_opt") sender ! paymentRequest - context.become(run(hash2preimage + (paymentHash -> (paymentPreimage, paymentRequest)))) + context.become(run(hash2preimage + (paymentHash -> PendingPaymentRequest(paymentPreimage, paymentRequest)))) } recover { case t => sender ! Status.Failure(t) } case CheckPayment(paymentHash) => @@ -69,14 +71,17 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin } case htlc: UpdateAddHtlc => - hash2preimage.get(htlc.paymentHash) match { - case Some((paymentPreimage, paymentRequest)) => + hash2preimage + .get(htlc.paymentHash) // we retrieve the request + .filterNot(hasExpired) // and filter it out if it is expired (it will be purged independently) + match { + case Some(PendingPaymentRequest(paymentPreimage, paymentRequest)) => val minFinalExpiry = Globals.blockCount.get() + paymentRequest.minFinalCltvExpiry.getOrElse(Channel.MIN_CLTV_EXPIRY) // The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however // it must not be greater than two times the requested amount. // see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#failure-messages paymentRequest.amount match { - case _ if htlc.expiry < minFinalExpiry => + 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}") @@ -95,9 +100,25 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin case None => sender ! CMD_FAIL_HTLC(htlc.id, Right(UnknownPaymentHash), commit = true) } + + case 'requests => + // this is just for testing + sender ! hash2preimage } + } object LocalPaymentHandler { + def props(nodeParams: NodeParams): Props = Props(new LocalPaymentHandler(nodeParams)) + + case object PurgeExpiredRequests + + case class PendingPaymentRequest(preimage: BinaryData, paymentRequest: PaymentRequest) + + def hasExpired(pr: PendingPaymentRequest): Boolean = pr.paymentRequest.expiry match { + case Some(expiry) => pr.paymentRequest.timestamp + expiry <= Platform.currentTime / 1000 + case None => false // this request will never expire + } + } \ No newline at end of file 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 afee2cdfa..9d6a4e9f8 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 @@ -200,7 +200,7 @@ object Relayer { case class FinalPayload(add: UpdateAddHtlc, payload: PerHopPayload) extends NextPayload case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: Sphinx.Packet) extends NextPayload { val relayFeeSatoshi = add.amountMsat - payload.amtToForward - val expiryDelta = add.expiry - payload.outgoingCltvValue + val expiryDelta = add.cltvExpiry - payload.outgoingCltvValue } // @formatter:on @@ -240,8 +240,8 @@ object Relayer { finalPayload.payload match { case PerHopPayload(_, finalAmountToForward, _) if finalAmountToForward > add.amountMsat => Left(CMD_FAIL_HTLC(add.id, Right(FinalIncorrectHtlcAmount(add.amountMsat)), commit = true)) - case PerHopPayload(_, _, finalOutgoingCltvValue) if finalOutgoingCltvValue != add.expiry => - Left(CMD_FAIL_HTLC(add.id, Right(FinalIncorrectCltvExpiry(add.expiry)), commit = true)) + case PerHopPayload(_, _, finalOutgoingCltvValue) if finalOutgoingCltvValue != add.cltvExpiry => + Left(CMD_FAIL_HTLC(add.id, Right(FinalIncorrectCltvExpiry(add.cltvExpiry)), commit = true)) case _ => Right(add) } @@ -265,7 +265,7 @@ object Relayer { case Some(channelUpdate) if payload.amtToForward < channelUpdate.htlcMinimumMsat => Left(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) if relayPayload.expiryDelta != channelUpdate.cltvExpiryDelta => - Left(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(add.expiry, channelUpdate)), commit = true)) + Left(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(add.cltvExpiry, channelUpdate)), commit = true)) case Some(channelUpdate) if relayPayload.relayFeeSatoshi < nodeFee(channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, payload.amtToForward) => Left(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => 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 b98be95b1..ae3073fde 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 @@ -200,7 +200,7 @@ object Transactions { val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec) .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash))))) val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash), htlc.add.expiry)))) + .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash), htlc.add.cltvExpiry)))) val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, locktime) = encodeTxNumber(txnumber) @@ -227,12 +227,12 @@ object Transactions { version = 2, txIn = TxIn(input.outPoint, Array.emptyByteArray, 0x00000000L) :: Nil, txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, - lockTime = htlc.expiry)) + lockTime = htlc.cltvExpiry)) } def makeHtlcSuccessTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcSuccessTx = { val fee = weight2fee(feeratePerKw, htlcSuccessWeight) - val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash), htlc.expiry) + val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash), 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 @@ -286,7 +286,7 @@ object Transactions { } def makeClaimHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: BinaryData, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = { - val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash), htlc.expiry) + val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash), htlc.cltvExpiry) val pubkeyScript = write(pay2wsh(redeemScript)) val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) @@ -296,7 +296,7 @@ object Transactions { version = 2, txIn = TxIn(input.outPoint, Array.emptyByteArray, 0x00000000L) :: Nil, txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = htlc.expiry) + lockTime = htlc.cltvExpiry) val weight = Transactions.addSigs(ClaimHtlcTimeoutTx(input, tx), BinaryData("00" * 73)).tx.weight() val fee = weight2fee(feeratePerKw, weight) 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 b0ec2489c..f1eba8feb 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 @@ -112,7 +112,7 @@ case class UpdateAddHtlc(channelId: BinaryData, id: Long, amountMsat: Long, paymentHash: BinaryData, - expiry: Long, + cltvExpiry: Long, onionRoutingPacket: BinaryData) extends HtlcMessage with UpdateMessage with HasChannelId case class UpdateFulfillHtlc(channelId: BinaryData, 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 e963db5b6..dea48d452 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 @@ -90,8 +90,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = BinaryData("42" * 32) - val originHtlc = UpdateAddHtlc(channelId = "42" * 32, id = 5656, amountMsat = 50000000, expiry = 400144, paymentHash = h, onionRoutingPacket = "00" * 1254) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.expiry - 7, upstream_opt = Some(originHtlc)) + val originHtlc = UpdateAddHtlc(channelId = "42" * 32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = "00" * 1254) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream_opt = Some(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -108,7 +108,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 42, expiry = 400144) + val add = CMD_ADD_HTLC(500000000, "11" * 42, cltvExpiry = 400144) sender.send(alice, add) val error = InvalidPaymentHash(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -121,7 +121,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, "11" * 32, expiry = expiryTooSmall) + val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = expiryTooSmall) 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(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -134,7 +134,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, "11" * 32, expiry = expiryTooBig) + val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = expiryTooBig) 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(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -257,7 +257,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && !alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isDefined) // actual test starts here - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144) + val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 400144) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -269,14 +269,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, "11" * 32, expiry = 400144) + val add1 = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 400144) 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, "22" * 32, expiry = 300000) + val add2 = CMD_ADD_HTLC(100000000, "22" * 32, cltvExpiry = 300000) // messages cross alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -328,7 +328,7 @@ 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("00" * 32, 0, 150, BinaryData("42" * 32), expiry = 400144, defaultOnion) + val htlc = UpdateAddHtlc("00" * 32, 0, 150, BinaryData("42" * 32), cltvExpiry = 400144, defaultOnion) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] assert(new String(error.data) === HtlcValueTooSmall(channelId(bob), minimum = 1000, actual = 150).getMessage) 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 e4b34d658..db066cf01 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 @@ -97,7 +97,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) + val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) 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 7de370327..9c3bb2b1f 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 @@ -69,7 +69,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods import f._ alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) + val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) 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 dcb840237..ce005351a 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 @@ -110,7 +110,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test starts here val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000) + val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) 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 fed98ada0..08b1c4030 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 @@ -99,7 +99,7 @@ class HtlcGenerationSpec extends FunSuite { val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) assert(add.amountMsat > finalAmountMsat) - assert(add.expiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) + assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash === paymentHash) assert(add.onion.length === Sphinx.PacketLength) @@ -133,7 +133,7 @@ class HtlcGenerationSpec extends FunSuite { val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops.take(1)) assert(add.amountMsat === finalAmountMsat) - assert(add.expiry === finalExpiry) + assert(add.cltvExpiry === finalExpiry) assert(add.paymentHash === paymentHash) assert(add.onion.size === Sphinx.PacketLength) 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 1ca28af3c..eb848066c 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 @@ -18,13 +18,14 @@ package fr.acinq.eclair.payment import akka.actor.Status.Failure import akka.actor.{ActorSystem, Status} -import akka.testkit.{TestKit, TestProbe} -import fr.acinq.bitcoin.{MilliSatoshi, Satoshi} +import akka.testkit.{TestActorRef, TestKit, TestProbe} +import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} +import fr.acinq.eclair.payment.LocalPaymentHandler.PendingPaymentRequest import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop -import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} +import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UnknownPaymentHash, UpdateAddHtlc} import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} import org.scalatest.FunSuiteLike @@ -38,7 +39,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike test("LocalPaymentHandler should reply with a fulfill/fail, emit a PaymentReceived and adds payment in DB") { val nodeParams = Alice.nodeParams - val handler = system.actorOf(LocalPaymentHandler.props(nodeParams)) + val handler = TestActorRef[LocalPaymentHandler](LocalPaymentHandler.props(nodeParams)) val sender = TestProbe() val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived]) @@ -55,7 +56,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] - assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat,add.paymentHash, add.channelId, timestamp = 0)) + assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Boolean] === true) } @@ -69,7 +70,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] - assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat,add.paymentHash, add.channelId, timestamp = 0)) + assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Boolean] === true) } @@ -79,13 +80,36 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Boolean] === false) - val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, expiry = Globals.blockCount.get() + 3, "") + val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, "") sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Boolean] === false) } + { + sender.send(handler, ReceivePayment(Some(amountMsat), "timeout expired", Some(1L))) + //allow request to timeout + Thread.sleep(1001) + val pr = sender.expectMsgType[PaymentRequest] + sender.send(handler, CheckPayment(pr.paymentHash)) + assert(sender.expectMsgType[Boolean] === false) + val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, expiry, "") + sender.send(handler, add) + assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(UnknownPaymentHash)) + // We chose UnknownPaymentHash on purpose. So if you have expired by 1 second or 1 hour you get the same error message. + eventListener.expectNoMsg(300 milliseconds) + sender.send(handler, CheckPayment(pr.paymentHash)) + assert(sender.expectMsgType[Boolean] === false) + // make sure that the request is indeed pruned + sender.send(handler, 'requests) + sender.expectMsgType[Map[BinaryData, PendingPaymentRequest]].contains(pr.paymentHash) + sender.send(handler, LocalPaymentHandler.PurgeExpiredRequests) + awaitCond({ + sender.send(handler, 'requests) + sender.expectMsgType[Map[BinaryData, PendingPaymentRequest]].contains(pr.paymentHash) == false + }) + } } test("Payment request generation should fail when the amount asked in not valid") { 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 7535d6163..3d2283928 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 @@ -66,7 +66,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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)) @@ -86,7 +86,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) sender.send(relayer, ForwardAdd(add_ab)) @@ -106,7 +106,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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)) @@ -132,7 +132,7 @@ class RelayerSpec extends TestkitBaseClass { // check that payments are sent properly val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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)) @@ -148,7 +148,7 @@ class RelayerSpec extends TestkitBaseClass { relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId) val (cmd1, _) = buildCommand(finalAmountMsat, finalExpiry, "02" * 32, hops) - val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.expiry, cmd1.onion) + val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) sender.send(relayer, ForwardAdd(add_ab)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message @@ -166,7 +166,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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)) @@ -187,7 +187,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, "00" * Sphinx.PacketLength) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, "00" * Sphinx.PacketLength) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -207,7 +207,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0)))) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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)) @@ -227,14 +227,14 @@ class RelayerSpec extends TestkitBaseClass { val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0))) val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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(IncorrectCltvExpiry(cmd.expiry, channelUpdate_bc))) + assert(fail.reason == Right(IncorrectCltvExpiry(cmd.cltvExpiry, channelUpdate_bc))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -247,7 +247,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(finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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)) @@ -268,7 +268,7 @@ class RelayerSpec extends TestkitBaseClass { val hops1 = hops.head :: Nil val (cmd, _) = buildCommand(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.expiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 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)) @@ -289,14 +289,14 @@ class RelayerSpec extends TestkitBaseClass { val hops1 = hops.head :: Nil val (cmd, _) = buildCommand(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.expiry - 1, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, 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)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(fail.id === add_ab.id) - assert(fail.reason == Right(FinalIncorrectCltvExpiry(add_ab.expiry))) + assert(fail.reason == Right(FinalIncorrectCltvExpiry(add_ab.cltvExpiry))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -340,7 +340,7 @@ 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 = "00" * 32, expiry = 4200, onionRoutingPacket = "") + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = "00" * 32, cltvExpiry = 4200, onionRoutingPacket = "") val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 42, paymentPreimage = "00" * 32) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFulfill(fulfill_ba, origin, add_bc)) @@ -358,7 +358,7 @@ 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 = "00" * 32, expiry = 4200, onionRoutingPacket = "") + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = "00" * 32, cltvExpiry = 4200, onionRoutingPacket = "") val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.createErrorPacket(BinaryData("01" * 32), TemporaryChannelFailure(channelUpdate_cd))) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFail(fail_ba, origin, add_bc)) 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 1bf7e63e5..2989ca04b 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 @@ -160,7 +160,7 @@ class TestVectorsSpec extends FunSuite with Logging { ) 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)) - case IN => Scripts.htlcReceived(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(htlc.add.paymentHash), htlc.add.expiry) + case IN => Scripts.htlcReceived(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(htlc.add.paymentHash), htlc.add.cltvExpiry) }) def dir2string(dir: Direction) = dir match { @@ -171,7 +171,7 @@ class TestVectorsSpec extends FunSuite with Logging { for (i <- 0 until htlcs.length) { logger.info(s"htlc $i direction: ${dir2string(htlcs(i).direction)}") logger.info(s"htlc $i amount_msat: ${htlcs(i).add.amountMsat}") - logger.info(s"htlc $i expiry: ${htlcs(i).add.expiry}") + logger.info(s"htlc $i expiry: ${htlcs(i).add.cltvExpiry}") logger.info(s"htlc $i payment_preimage: ${paymentPreimages(i)}") } 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 e8f2d537e..7c4f05470 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 @@ -124,8 +124,8 @@ 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 = BinaryData("42" * 32) - val htlc = UpdateAddHtlc("00" * 32, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), expiry = 400144, BinaryData.empty) - val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.expiry) + val htlc = UpdateAddHtlc("00" * 32, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, BinaryData.empty) + 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 htlcPenaltyTx = makeHtlcPenaltyTx(commitTx, outputsAlreadyUsed = Set.empty, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) @@ -139,7 +139,7 @@ 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 = BinaryData("42" * 32) - val htlc = UpdateAddHtlc("00" * 32, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), expiry = 400144, BinaryData.empty) + val htlc = UpdateAddHtlc("00" * 32, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, BinaryData.empty) 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 claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) @@ -153,8 +153,8 @@ 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 = BinaryData("42" * 32) - val htlc = UpdateAddHtlc("00" * 32, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), expiry = 400144, BinaryData.empty) - val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.expiry))) + val htlc = UpdateAddHtlc("00" * 32, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, BinaryData.empty) + 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 claimClaimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) // we use dummy signatures to compute the weight @@ -304,7 +304,7 @@ class TransactionsSpec extends FunSuite with Logging { { // remote spends received HTLC output with revocation key - val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.expiry)) + val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.cltvExpiry)) val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, outputsAlreadyUsed = Set.empty, script, localDustLimit, finalPubKeyScript, feeratePerKw) val sig = sign(htlcPenaltyTx, localRevocationPriv) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) 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 40d328c3e..87e0c8e41 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 @@ -104,7 +104,7 @@ class ChannelCodecsSpec extends FunSuite { channelId = randomBytes(32), id = Random.nextInt(Int.MaxValue), amountMsat = Random.nextInt(Int.MaxValue), - expiry = Random.nextInt(Int.MaxValue), + cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes(32), onionRoutingPacket = randomBytes(Sphinx.PacketLength)) val htlc1 = DirectedHtlc(direction = IN, add = add) @@ -118,14 +118,14 @@ class ChannelCodecsSpec extends FunSuite { channelId = randomBytes(32), id = Random.nextInt(Int.MaxValue), amountMsat = Random.nextInt(Int.MaxValue), - expiry = Random.nextInt(Int.MaxValue), + cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes(32), onionRoutingPacket = randomBytes(Sphinx.PacketLength)) val add2 = UpdateAddHtlc( channelId = randomBytes(32), id = Random.nextInt(Int.MaxValue), amountMsat = Random.nextInt(Int.MaxValue), - expiry = Random.nextInt(Int.MaxValue), + cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes(32), onionRoutingPacket = randomBytes(Sphinx.PacketLength)) val htlc1 = DirectedHtlc(direction = IN, add = add1) From 65a854caf313fbf9155705e32a291eade170864e Mon Sep 17 00:00:00 2001 From: Anton Kumaigorodski Date: Tue, 20 Nov 2018 15:03:46 +0200 Subject: [PATCH 30/31] Disconnect peer if it does not reply to pings (#755) * Also ignore invalid pings --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 246 ++++++++++-------- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 39 ++- 2 files changed, 175 insertions(+), 110 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 10c078bcc..d01f29e9f 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 @@ -20,19 +20,19 @@ import java.io.ByteArrayInputStream import java.net.InetSocketAddress import java.nio.ByteOrder -import akka.actor.{ActorRef, OneForOneStrategy, PoisonPill, Props, Status, SupervisorStrategy, Terminated} +import akka.actor.{ActorRef, Cancellable, OneForOneStrategy, PoisonPill, Props, Status, SupervisorStrategy, Terminated} import akka.event.Logging.MDC import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, MilliSatoshi, Protocol, Satoshi} import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} import scodec.Attempt +import scala.compat.Platform import scala.concurrent.duration._ import scala.util.Random @@ -72,7 +72,8 @@ 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, remoteNodeId1, address, outgoing, origin_opt), d: DisconnectedData) => + require(remoteNodeId == remoteNodeId1, s"invalid nodeid: $remoteNodeId != $remoteNodeId1") log.debug(s"got authenticated connection to $remoteNodeId@${address.getHostString}:${address.getPort}") transport ! TransportHandler.Listener(self) context watch transport @@ -82,10 +83,10 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor 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, d.channels, origin_opt) case Event(Terminated(actor), d@DisconnectedData(_, channels, _)) if channels.exists(_._2 == actor) => - val h = channels.filter(_._2 == actor).map(_._1) + val h = channels.filter(_._2 == actor).keys log.info(s"channel closed: channelId=${h.mkString("/")}") stay using d.copy(channels = channels -- h) @@ -93,14 +94,14 @@ 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) + 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) log.info(s"$remoteNodeId has features: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") if (Features.areSupported(remoteInit.localFeatures)) { - origin_opt.map(origin => origin ! "connected") + d.origin_opt.foreach(origin => origin ! "connected") if (remoteHasInitialRoutingSync) { if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { @@ -113,22 +114,22 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { // if they support channel queries, always ask for their filter - router ! SendChannelQuery(remoteNodeId, transport) + router ! SendChannelQuery(remoteNodeId, d.transport) } // 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) }) + d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(d.transport)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) + goto(CONNECTED) using ConnectedData(d.address_opt, d.transport, remoteInit, d.channels.map { case (k: ChannelId, v) => (k, v) }) } else { log.warning(s"incompatible features, disconnecting") - origin_opt.map(origin => origin ! Status.Failure(new RuntimeException("incompatible features"))) - transport ! PoisonPill + d.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible features"))) + d.transport ! PoisonPill stay } case Event(Authenticator.Authenticated(connection, _, _, _, _, origin_opt), _) => // two connections in parallel - origin_opt.map(origin => origin ! Status.Failure(new RuntimeException("there is another connection attempt in progress"))) + origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("there is another connection attempt in progress"))) // we kill this one log.warning(s"killing parallel connection $connection") connection ! PoisonPill @@ -140,105 +141,122 @@ 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), d: InitializingData) if actor == d.transport => log.warning(s"lost connection to $remoteNodeId") - goto(DISCONNECTED) using DisconnectedData(address_opt, channels) + goto(DISCONNECTED) using DisconnectedData(d.address_opt, d.channels) - case Event(Terminated(actor), d@InitializingData(_, _, channels, _)) if channels.exists(_._2 == actor) => - val h = channels.filter(_._2 == actor).map(_._1) + case Event(Terminated(actor), d: InitializingData) if d.channels.exists(_._2 == actor) => + val h = d.channels.filter(_._2 == actor).keys log.info(s"channel closed: channelId=${h.mkString("/")}") - stay using d.copy(channels = channels -- h) + stay using d.copy(channels = d.channels -- h) } - when(CONNECTED, stateTimeout = nodeParams.pingInterval) { - case Event(StateTimeout, ConnectedData(_, transport, _, _, _, _)) => + when(CONNECTED) { + case Event(SendPing, d: ConnectedData) => // no need to use secure random here val pingSize = Random.nextInt(1000) val pongSize = Random.nextInt(1000) - transport ! wire.Ping(pongSize, BinaryData("00" * pingSize)) + val ping = wire.Ping(pongSize, BinaryData("00" * pingSize)) + setTimer(PingTimeout.toString, PingTimeout(ping), 10 seconds, repeat = false) + d.transport ! ping + stay using d.copy(expectedPong_opt = Some(ExpectedPong(ping))) + + case Event(PingTimeout(ping), d: ConnectedData) => + log.warning(s"no response to ping=$ping, closing connection") + d.transport ! PoisonPill stay - 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) { - transport ! wire.Pong(BinaryData("00" * pongLength)) + case Event(ping@wire.Ping(pongLength, _), d: ConnectedData) => + d.transport ! TransportHandler.ReadAck(ping) + if (pongLength <= 65532) { + // see BOLT 1: we reply only if requested pong length is acceptable + d.transport ! wire.Pong(BinaryData(Seq.fill[Byte](pongLength)(0.toByte))) + } else { + log.warning(s"ignoring invalid ping with pongLength=${ping.pongLength}") } stay - 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(pong@wire.Pong(data), d: ConnectedData) => + d.transport ! TransportHandler.ReadAck(pong) + d.expectedPong_opt match { + case Some(ExpectedPong(ping, timestamp)) if ping.pongLength == data.length => + // we use the pong size to correlate between pings and pongs + val latency = Platform.currentTime - timestamp + log.debug(s"received pong with latency=$latency") + cancelTimer(PingTimeout.toString()) + schedulePing() + case None => + log.debug(s"received unexpected pong with size=${data.length}") + } + stay using d.copy(expectedPong_opt = None) - case Event(err@wire.Error(channelId, reason), ConnectedData(_, transport, _, channels, _, _)) if channelId == CHANNELID_ZERO => - transport ! TransportHandler.ReadAck(err) + case Event(err@wire.Error(channelId, reason), d: ConnectedData) if channelId == CHANNELID_ZERO => + d.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 + d.channels.values.toSet[ActorRef].foreach(_ forward err) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) + d.transport ! PoisonPill stay - case Event(err: wire.Error, ConnectedData(_, transport, _, channels, _, _)) => - transport ! TransportHandler.ReadAck(err) + case Event(err: wire.Error, d: ConnectedData) => + d.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 { + d.channels.get(FinalChannelId(err.channelId)).orElse(d.channels.get(TemporaryChannelId(err.channelId))) match { case Some(channel) => channel forward err - case None => transport ! wire.Error(err.channelId, UNKNOWN_CHANNEL_MESSAGE) + case None => d.transport ! wire.Error(err.channelId, UNKNOWN_CHANNEL_MESSAGE) } stay - case Event(c: Peer.OpenChannel, d@ConnectedData(_, transport, remoteInit, channels, _, _)) => + case Event(c: Peer.OpenChannel, d: ConnectedData) => 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) val channelFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.blocks_6) - 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)) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, 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(_, transport, remoteInit, channels, _, _)) => - transport ! TransportHandler.ReadAck(msg) - channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { + case Event(msg: wire.OpenChannel, d: ConnectedData) => + d.transport ! TransportHandler.ReadAck(msg) + d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => log.info(s"accepting a new channel to $remoteNodeId") val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingSatoshis = msg.fundingSatoshis, origin_opt = None) val temporaryChannelId = msg.temporaryChannelId - channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, transport, remoteInit) + channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.transport, d.remoteInit) channel ! msg - stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Some(_) => log.warning(s"ignoring open_channel with duplicate temporaryChannelId=${msg.temporaryChannelId}") stay } - case Event(msg: wire.HasChannelId, ConnectedData(_, transport, _, channels, _, _)) => - transport ! TransportHandler.ReadAck(msg) - channels.get(FinalChannelId(msg.channelId)) match { + case Event(msg: wire.HasChannelId, d: ConnectedData) => + d.transport ! TransportHandler.ReadAck(msg) + d.channels.get(FinalChannelId(msg.channelId)) match { case Some(channel) => channel forward msg - case None => transport ! wire.Error(msg.channelId, UNKNOWN_CHANNEL_MESSAGE) + case None => d.transport ! wire.Error(msg.channelId, UNKNOWN_CHANNEL_MESSAGE) } stay - case Event(msg: wire.HasTemporaryChannelId, ConnectedData(_, transport, _, channels, _, _)) => - transport ! TransportHandler.ReadAck(msg) - channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { + case Event(msg: wire.HasTemporaryChannelId, d: ConnectedData) => + d.transport ! TransportHandler.ReadAck(msg) + d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case Some(channel) => channel forward msg - case None => transport ! wire.Error(msg.temporaryChannelId, UNKNOWN_CHANNEL_MESSAGE) + case None => d.transport ! wire.Error(msg.temporaryChannelId, UNKNOWN_CHANNEL_MESSAGE) } stay - case Event(ChannelIdAssigned(channel, _, temporaryChannelId, channelId), d@ConnectedData(_, _, _, channels, _, _)) if channels.contains(TemporaryChannelId(temporaryChannelId)) => + case Event(ChannelIdAssigned(channel, _, temporaryChannelId, channelId), d: ConnectedData) if d.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)) + stay using d.copy(channels = d.channels + (FinalChannelId(channelId) -> channel)) - case Event(RoutingState(channels, updates, nodes), ConnectedData(_, transport, _, _, _, _)) => + case Event(RoutingState(channels, updates, nodes), d: ConnectedData) => // let's send the messages def send(announcements: Iterable[_ <: LightningMessage]) = announcements.foldLeft(0) { case (c, ann) => - transport ! ann + d.transport ! ann c + 1 } @@ -249,7 +267,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, d: ConnectedData) => /** * Send and count in a single iteration @@ -258,11 +276,11 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor case (count, (_, origins)) if origins.contains(self) => // the announcement came from this peer, we don't send it back count - case (count, (msg: HasTimestamp, _)) if !timestampInRange(msg, maybeGossipTimestampFilter) => + case (count, (msg: HasTimestamp, _)) if !timestampInRange(msg, d.gossipTimestampFilter) => // the peer has set up a filter on timestamp and this message is out of range count case (count, (msg, _)) => - transport ! msg + d.transport ! msg count + 1 } @@ -275,7 +293,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } stay - case Event(msg: GossipTimestampFilter, data: ConnectedData) => + case Event(msg: GossipTimestampFilter, d: ConnectedData) => // special case: time range filters are peer specific and must not be sent to the router sender ! TransportHandler.ReadAck(msg) if (msg.chainHash != nodeParams.chainHash) { @@ -284,26 +302,26 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } else { log.info(s"setting up gossipTimestampFilter=$msg") // update their timestamp filter - stay using data.copy(gossipTimestampFilter = Some(msg)) + stay using d.copy(gossipTimestampFilter = Some(msg)) } - case Event(msg: wire.RoutingMessage, ConnectedData(_, transport, _, _, _, behavior)) => + case Event(msg: wire.RoutingMessage, d: ConnectedData) => msg match { - case _: ChannelAnnouncement | _: ChannelUpdate | _: NodeAnnouncement if behavior.ignoreNetworkAnnouncement => + case _: ChannelAnnouncement | _: ChannelUpdate | _: NodeAnnouncement if d.behavior.ignoreNetworkAnnouncement => // this peer is currently under embargo! sender ! TransportHandler.ReadAck(msg) case _ => // Note: we don't ack messages here because we don't want them to be stacked in the router's mailbox - router ! PeerRoutingMessage(transport, remoteNodeId, msg) + router ! PeerRoutingMessage(d.transport, remoteNodeId, msg) } stay - case Event(readAck: TransportHandler.ReadAck, ConnectedData(_, transport, _, _, _, _)) => + case Event(readAck: TransportHandler.ReadAck, d: ConnectedData) => // we just forward acks from router to transport - transport forward readAck + d.transport forward readAck stay - case Event(badMessage: BadMessage, data@ConnectedData(_, transport, _, _, _, behavior)) => + case Event(badMessage: BadMessage, d: ConnectedData) => val behavior1 = badMessage match { case InvalidSignature(r) => val bin: String = LightningMessageCodecs.lightningMessageCodec.encode(r) match { @@ -312,66 +330,64 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor } log.error(s"peer sent us a routing message with invalid sig: r=$r bin=$bin") // for now we just return an error, maybe ban the peer in the future? - transport ! Error(CHANNELID_ZERO, s"bad announcement sig! bin=$bin".getBytes()) - behavior + d.transport ! Error(CHANNELID_ZERO, s"bad announcement sig! bin=$bin".getBytes()) + d.behavior case ChannelClosed(_) => - if (behavior.ignoreNetworkAnnouncement) { + if (d.behavior.ignoreNetworkAnnouncement) { // we already are ignoring announcements, we may have additional notifications for announcements that were received right before our ban - behavior.copy(fundingTxAlreadySpentCount = behavior.fundingTxAlreadySpentCount + 1) - } else if (behavior.fundingTxAlreadySpentCount < MAX_FUNDING_TX_ALREADY_SPENT) { - behavior.copy(fundingTxAlreadySpentCount = behavior.fundingTxAlreadySpentCount + 1) + d.behavior.copy(fundingTxAlreadySpentCount = d.behavior.fundingTxAlreadySpentCount + 1) + } else if (d.behavior.fundingTxAlreadySpentCount < MAX_FUNDING_TX_ALREADY_SPENT) { + d.behavior.copy(fundingTxAlreadySpentCount = d.behavior.fundingTxAlreadySpentCount + 1) } else { - log.warning(s"peer sent us too many channel announcements with funding tx already spent (count=${behavior.fundingTxAlreadySpentCount + 1}), ignoring network announcements for $IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD") - import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce(IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD, self, ResumeAnnouncements) - behavior.copy(fundingTxAlreadySpentCount = behavior.fundingTxAlreadySpentCount + 1, ignoreNetworkAnnouncement = true) + log.warning(s"peer sent us too many channel announcements with funding tx already spent (count=${d.behavior.fundingTxAlreadySpentCount + 1}), ignoring network announcements for $IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD") + setTimer(ResumeAnnouncements.toString, ResumeAnnouncements, IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD, repeat = false) + d.behavior.copy(fundingTxAlreadySpentCount = d.behavior.fundingTxAlreadySpentCount + 1, ignoreNetworkAnnouncement = true) } case NonexistingChannel(_) => // this should never happen, unless we are not in sync or there is a 6+ blocks reorg - if (behavior.ignoreNetworkAnnouncement) { + if (d.behavior.ignoreNetworkAnnouncement) { // we already are ignoring announcements, we may have additional notifications for announcements that were received right before our ban - behavior.copy(fundingTxNotFoundCount = behavior.fundingTxNotFoundCount + 1) - } else if (behavior.fundingTxNotFoundCount < MAX_FUNDING_TX_NOT_FOUND) { - behavior.copy(fundingTxNotFoundCount = behavior.fundingTxNotFoundCount + 1) + d.behavior.copy(fundingTxNotFoundCount = d.behavior.fundingTxNotFoundCount + 1) + } else if (d.behavior.fundingTxNotFoundCount < MAX_FUNDING_TX_NOT_FOUND) { + d.behavior.copy(fundingTxNotFoundCount = d.behavior.fundingTxNotFoundCount + 1) } else { - log.warning(s"peer sent us too many channel announcements with non-existing funding tx (count=${behavior.fundingTxNotFoundCount + 1}), ignoring network announcements for $IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD") - import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce(IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD, self, ResumeAnnouncements) - behavior.copy(fundingTxNotFoundCount = behavior.fundingTxNotFoundCount + 1, ignoreNetworkAnnouncement = true) + log.warning(s"peer sent us too many channel announcements with non-existing funding tx (count=${d.behavior.fundingTxNotFoundCount + 1}), ignoring network announcements for $IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD") + setTimer(ResumeAnnouncements.toString, ResumeAnnouncements, IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD, repeat = false) + d.behavior.copy(fundingTxNotFoundCount = d.behavior.fundingTxNotFoundCount + 1, ignoreNetworkAnnouncement = true) } } - stay using data.copy(behavior = behavior1) + stay using d.copy(behavior = behavior1) - case Event(ResumeAnnouncements, data: ConnectedData) => + case Event(ResumeAnnouncements, d: ConnectedData) => log.info(s"resuming processing of network announcements for peer") - stay using data.copy(behavior = data.behavior.copy(fundingTxAlreadySpentCount = 0, ignoreNetworkAnnouncement = false)) + stay using d.copy(behavior = d.behavior.copy(fundingTxAlreadySpentCount = 0, ignoreNetworkAnnouncement = false)) - case Event(Disconnect, ConnectedData(_, transport, _, _, _, _)) => - transport ! PoisonPill + case Event(Disconnect, d: ConnectedData) => + d.transport ! PoisonPill stay - case Event(Terminated(actor), ConnectedData(address_opt, transport, _, channels, _, _)) if actor == transport => + case Event(Terminated(actor), d: ConnectedData) if actor == d.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) }) + d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) + goto(DISCONNECTED) using DisconnectedData(d.address_opt, d.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) if d.channels.values.toSet.contains(actor) => // we will have at most 2 ids: a TemporaryChannelId and a FinalChannelId - val channelIds = channels.filter(_._2 == actor).keys + val channelIds = d.channels.filter(_._2 == actor).keys log.info(s"channel closed: channelId=${channelIds.mkString("/")}") - if (channels.values.toSet - actor == Set.empty) { + if (d.channels.values.toSet - actor == Set.empty) { log.info(s"that was the last open channel, closing the connection") - transport ! PoisonPill + d.transport ! PoisonPill } - stay using d.copy(channels = channels -- channelIds) + stay using d.copy(channels = d.channels -- channelIds) - case Event(h: Authenticator.Authenticated, ConnectedData(address_opt, oldTransport, _, channels, _, _)) => + case Event(h: Authenticator.Authenticated, d: ConnectedData) => log.info(s"got new transport while already connected, switching to new transport") - context unwatch oldTransport - oldTransport ! PoisonPill - channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) + context unwatch d.transport + d.transport ! PoisonPill + d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) self ! h - goto(DISCONNECTED) using DisconnectedData(address_opt, channels.collect { case (k: FinalChannelId, v) => (k, v) }) + goto(DISCONNECTED) using DisconnectedData(d.address_opt, d.channels.collect { case (k: FinalChannelId, v) => (k, v) }) } whenUnhandled { @@ -392,11 +408,16 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor case Event(_: RoutingState, _) => stay // ignored case Event(_: TransportHandler.ReadAck, _) => stay // ignored + + case Event(SendPing, _) => stay // we got disconnected in the meantime + + case Event(_: Pong, _) => stay // we got disconnected before receiving the pong } onTransition { case _ -> DISCONNECTED if nodeParams.autoReconnect && nextStateData.address_opt.isDefined => setTimer(RECONNECT_TIMER, Reconnect, 1 second, repeat = false) case DISCONNECTED -> _ if nodeParams.autoReconnect && stateData.address_opt.isDefined => cancelTimer(RECONNECT_TIMER) + case _ -> CONNECTED => schedulePing() } def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingSatoshis: Long, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { @@ -419,6 +440,12 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor override def mdc(currentMessage: Any): MDC = Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) + def schedulePing(): Unit = { + // pings are periodically with some randomization + val nextDelay = nodeParams.pingInterval + secureRandom.nextInt(10).seconds + setTimer(SendPing.toString, SendPing, nextDelay, repeat = false) + } + } object Peer { @@ -450,7 +477,9 @@ object Peer { 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, behavior: Behavior = Behavior()) extends Data + case class ConnectedData(address_opt: Option[InetSocketAddress], transport: ActorRef, remoteInit: wire.Init, channels: Map[ChannelId, ActorRef], gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None) extends Data + case class ExpectedPong(ping: Ping, timestamp: Long = Platform.currentTime) + case class PingTimeout(ping: Ping) sealed trait State case object INSTANTIATING extends State @@ -471,6 +500,7 @@ object Peer { require(fundingTxFeeratePerKw_opt.getOrElse(0L) >= 0, s"funding tx feerate must be positive") } case object GetPeerInfo + case object SendPing case class PeerInfo(nodeId: PublicKey, state: String, address: Option[InetSocketAddress], channels: Int) case class PeerRoutingMessage(transport: ActorRef, remoteNodeId: PublicKey, message: RoutingMessage) 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 347d86a4b..21ae51b30 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,14 +4,15 @@ import java.net.InetSocketAddress import akka.actor.ActorRef import akka.testkit.TestProbe +import fr.acinq.eclair.randomBytes import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ 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.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements, SendPing} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} -import fr.acinq.eclair.wire.Error +import fr.acinq.eclair.wire.{Error, Ping, Pong} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, wire} import org.scalatest.Outcome @@ -54,6 +55,39 @@ class PeerSpec extends TestkitBaseClass { assert(probe.expectMsgType[Peer.PeerInfo].state == "CONNECTED") } + test("reply to ping") { f => + import f._ + val probe = TestProbe() + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + val ping = Ping(42, randomBytes(127)) + probe.send(peer, ping) + transport.expectMsg(TransportHandler.ReadAck(ping)) + assert(transport.expectMsgType[Pong].data.size === ping.pongLength) + } + + test("ignore malicious ping") { f => + import f._ + val probe = TestProbe() + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + // huge requested pong length + val ping = Ping(Int.MaxValue, randomBytes(127)) + probe.send(peer, ping) + transport.expectMsg(TransportHandler.ReadAck(ping)) + transport.expectNoMsg() + } + + test("disconnect if no reply to ping") { f => + import f._ + val sender = TestProbe() + val deathWatcher = TestProbe() + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) + // we manually trigger a ping because we don't want to wait too long in tests + sender.send(peer, SendPing) + transport.expectMsgType[Ping] + deathWatcher.watch(transport.ref) + deathWatcher.expectTerminated(transport.ref, max = 11 seconds) + } + test("filter gossip message (no filtering)") { f => import f._ val probe = TestProbe() @@ -165,4 +199,5 @@ class PeerSpec extends TestkitBaseClass { assert(error.channelId === CHANNELID_ZERO) assert(new String(error.data).startsWith("bad announcement sig! bin=0100")) } + } From fa1b2e4c0036c5a9eb8ef783972e1375f9c1f91b Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 23 Nov 2018 11:58:23 +0100 Subject: [PATCH 31/31] Switch sttp backend async-http-client -> okhttp (#764) Okhttp works better on Android. --- eclair-core/pom.xml | 2 +- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 4 ++-- .../acinq/eclair/blockchain/bitcoind/BitcoindService.scala | 5 +++-- .../eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala | 4 ++-- .../eclair/router/AnnouncementsBatchValidationSpec.scala | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index b842b4624..b2cc942e7 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -135,7 +135,7 @@ com.softwaremill.sttp - async-http-client-backend-future_${scala.version.short} + okhttp-backend_${scala.version.short} ${sttp.version} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index ea6b24c9f..df860ca3d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -24,7 +24,7 @@ import akka.http.scaladsl.Http import akka.pattern.after import akka.stream.{ActorMaterializer, BindFailedException} import akka.util.Timeout -import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend +import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} @@ -83,7 +83,7 @@ class Setup(datadir: File, secureRandom.nextInt() implicit val ec = ExecutionContext.Implicits.global - implicit val sttpBackend = AsyncHttpClientFutureBackend() + implicit val sttpBackend = OkHttpFutureBackend() val bitcoin = nodeParams.watcherType match { case BITCOIND => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index edabdebd6..8c9cc28d2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -23,7 +23,8 @@ import java.util.UUID import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.pattern.pipe import akka.testkit.{TestKitBase, TestProbe} -import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend +import com.softwaremill.sttp.okhttp.OkHttpFutureBackend +import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient} import fr.acinq.eclair.integration.IntegrationSpec import grizzled.slf4j.Logging @@ -36,7 +37,7 @@ trait BitcoindService extends Logging { self: TestKitBase => implicit val system: ActorSystem - implicit val sttpBackend = AsyncHttpClientFutureBackend() + implicit val sttpBackend = OkHttpFutureBackend() import scala.sys.process._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala index d8197bc95..fb3c9c85a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.blockchain.fee import akka.util.Timeout -import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend +import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import grizzled.slf4j.Logging import org.json4s.DefaultFormats import org.scalatest.FunSuite @@ -70,7 +70,7 @@ class EarnDotComFeeProviderSpec extends FunSuite with Logging { test("make sure API hasn't changed") { import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ - implicit val sttpBackend = AsyncHttpClientFutureBackend() + implicit val sttpBackend = OkHttpFutureBackend() implicit val timeout = Timeout(30 seconds) val provider = new EarnDotComFeeProvider() logger.info("earn.com livenet fees: " + Await.result(provider.getFeerates, 10 seconds)) 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 de0986e5d..3ebaabdf8 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 @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.pattern.pipe import akka.testkit.TestProbe -import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend +import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction} import fr.acinq.eclair.blockchain.ValidateResult @@ -45,7 +45,7 @@ class AnnouncementsBatchValidationSpec extends FunSuite { import scala.concurrent.ExecutionContext.Implicits.global implicit val system = ActorSystem() - implicit val sttpBackend = AsyncHttpClientFutureBackend() + implicit val sttpBackend = OkHttpFutureBackend() implicit val extendedBitcoinClient = new ExtendedBitcoinClient(new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 18332)) val channels = for (i <- 0 until 50) yield {