diff --git a/README.md b/README.md index 76793a79d..ebf175cc7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **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 [lightning-c], [lit], and [lnd]. +This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning] and [lnd]. --- @@ -179,9 +179,6 @@ docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConso - [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell - [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to -[Amiko-Pay]: https://github.com/cornwarecjp/amiko-pay -[lightning-c]: https://github.com/ElementsProject/lightning +[c-lightning]: https://github.com/ElementsProject/lightning [lnd]: https://github.com/LightningNetwork/lnd -[lit]: https://github.com/mit-dci/lit -[Thunder]: https://github.com/blockchain/thunder diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index c18fd8deb..147411131 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -5,7 +5,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-android-alpha13 + 0.2-android-SNAPSHOT eclair-core_2.11 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 af720c396..f444d9070 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 @@ -123,9 +123,25 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data] } } - case Event(LocalChannelDown(_, channelId, shortChannelId, _), d: Data) => - log.debug("removed local channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) - stay using d.copy(privateChannels = d.privateChannels - shortChannelId, privateUpdates = d.privateUpdates.filterKeys(_.shortChannelId != shortChannelId)) + case Event(LocalChannelDown(_, channelId, shortChannelId, remoteNodeId), d: Data) => + // a local channel has permanently gone down + if (d.channels.contains(shortChannelId)) { + // the channel was public, we will receive (or have already received) a WatchEventSpentBasic event, that will trigger a clean up of the channel + // so let's not do anything here + stay + } else if (d.privateChannels.contains(shortChannelId)) { + // the channel was private or public-but-not-yet-announced, let's do the clean up + log.debug("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) + val desc1 = ChannelDesc(shortChannelId, nodeParams.nodeId, remoteNodeId) + val desc2 = ChannelDesc(shortChannelId, remoteNodeId, nodeParams.nodeId) + // we remove the corresponding updates from the graph + removeEdge(d.graph, desc1) + removeEdge(d.graph, desc2) + // and we remove the channel and channel_update from our state + stay using d.copy(privateChannels = d.privateChannels - shortChannelId, privateUpdates = d.privateUpdates - desc1 - desc2) + } else { + stay + } case Event(c: ChannelAnnouncement, d) => log.debug("received channel announcement for shortChannelId={} nodeId1={} nodeId2={} from {}", c.shortChannelId, c.nodeId1, c.nodeId2, sender) @@ -163,8 +179,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data] log.debug("received channel update for shortChannelId={} from {}", u.shortChannelId, sender) stay using handle(u, sender, d) - case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d) - if d.channels.contains(shortChannelId) => + case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d) if d.channels.contains(shortChannelId) => val lostChannel = d.channels(shortChannelId) log.info("funding tx of channelId={} has been spent", shortChannelId) // we need to remove nodes that aren't tied to any channels anymore 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 575e583d2..cfffe1d40 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 @@ -1,9 +1,11 @@ package fr.acinq.eclair.payment import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} +import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.MilliSatoshi import fr.acinq.eclair.Globals +import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.ErrorPacket @@ -72,7 +74,33 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router sender.expectMsg(PaymentFailed(request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) + } + test("payment failed (local error)") { case (router, _) => + val relayer = TestProbe() + val routerForwarder = TestProbe() + val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val monitor = TestProbe() + val sender = TestProbe() + + paymentFSM ! SubscribeTransitionCallBack(monitor.ref) + val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) + + val request = SendPayment(142000L, "42" * 32, d, maxAttempts = 2) + sender.send(paymentFSM, request) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + routerForwarder.expectMsg(RouteRequest(a, d, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.forward(router) + awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) + val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData + + relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) + sender.send(paymentFSM, Status.Failure(AddHtlcFailed("00" * 32, request.paymentHash, ChannelUnavailable("00" * 32), Local(Some(paymentFSM.underlying.self)), None))) + + // then the payment lifecycle will ask for a new route excluding the channel + routerForwarder.expectMsg(RouteRequest(a, d, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(channelId_ab))) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { case (router, _) => @@ -100,7 +128,6 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(channelId_ab))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - } test("payment failed (TemporaryChannelFailure)") { case (router, _) => @@ -193,7 +220,6 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(paymentOK.amountMsat > request.amountMsat) val PaymentSent(MilliSatoshi(request.amountMsat), feesPaid, request.paymentHash, paymentOK.paymentPreimage) = eventListener.expectMsgType[PaymentSent] assert(feesPaid.amount > 0) - } } diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 707169bbd..148aceed9 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -5,7 +5,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-android-alpha13 + 0.2-android-SNAPSHOT eclair-node_2.11 diff --git a/pom.xml b/pom.xml index 3c80613fe..00dc008d2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-android-alpha13 + 0.2-android-SNAPSHOT pom