Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- `audit` now accepts `--count` and `--skip` parameters to limit the number of retrieved items (#2474, #2487)
- `sendtoroute` removes the `--trampolineNodes` argument and implicitly uses a single trampoline hop (#2480)
- `payinvoice` always returns the payment result when used with `--blocking`, even when using MPP (#2525)
- `node` returns high-level information about a remote node (#2568)

### Miscellaneous improvements and bug fixes

Expand Down
1 change: 1 addition & 0 deletions eclair-core/eclair-cli
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ and COMMAND is one of the available commands:
- findroute
- findroutetonode
- findroutebetweennodes
- node
- nodes

=== Invoice ===
Expand Down
9 changes: 9 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ trait Eclair {

def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]]

def node(nodeId: PublicKey)(implicit timeout: Timeout): Future[Option[Router.PublicNode]]

def nodes(nodeIds_opt: Option[Set[PublicKey]] = None)(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]]

def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice]
Expand Down Expand Up @@ -223,6 +225,13 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo(None)).mapTo[PeerInfo]))
} yield peerinfos

override def node(nodeId: PublicKey)(implicit timeout: Timeout): Future[Option[Router.PublicNode]] = {
(appKit.router ? Router.GetNode(nodeId)).mapTo[Router.GetNodeResponse].map {
case n: PublicNode => Some(n)
case _: UnknownNode => None
}
}

override def nodes(nodeIds_opt: Option[Set[PublicKey]])(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] = {
(appKit.router ? Router.GetNodes)
.mapTo[Iterable[NodeAnnouncement]]
Expand Down
20 changes: 18 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm
sender() ! d.excludedChannels
stay()

case Event(GetNode(nodeId), d) =>
d.nodes.get(nodeId) match {
case Some(announcement) =>
// This only provides a lower bound on the number of channels this peer has: disabled channels will be filtered out.
val activeChannels = d.graphWithBalances.graph.getIncomingEdgesOf(nodeId)
val totalCapacity = activeChannels.map(_.capacity).sum
sender() ! PublicNode(announcement, activeChannels.size, totalCapacity)
case None =>
sender() ! UnknownNode(nodeId)
}
stay()

case Event(GetNodes, d) =>
sender() ! d.nodes.values
stay()
Expand Down Expand Up @@ -639,12 +651,11 @@ object Router {
/** This is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed) */
case class ExcludeChannel(desc: ChannelDesc, duration_opt: Option[FiniteDuration])
case class LiftChannelExclusion(desc: ChannelDesc)
case object GetExcludedChannels

sealed trait ExcludedChannelStatus
case object ExcludedForever extends ExcludedChannelStatus
case class ExcludedUntil(liftExclusionAt: TimestampSecond, timer: Cancellable) extends ExcludedChannelStatus

case object GetExcludedChannels
// @formatter:on

// @formatter:off
Expand All @@ -658,6 +669,11 @@ object Router {
case object GetChannels
case object GetChannelsMap
case object GetChannelUpdates

case class GetNode(nodeId: PublicKey)
sealed trait GetNodeResponse
case class PublicNode(announcement: NodeAnnouncement, activeChannels: Int, totalCapacity: Satoshi) extends GetNodeResponse
case class UnknownNode(nodeId: PublicKey) extends GetNodeResponse
// @formatter:on

// @formatter:off
Expand Down
19 changes: 19 additions & 0 deletions eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,25 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
assertThrows[IllegalArgumentException](Await.result(eclair.send(None, 123 msat, expiredInvoice), 50 millis))
}

test("return node details") { f =>
import f._

val eclair = new EclairImpl(kit)

val ann = NodeAnnouncement(randomBytes64(), Features.empty, TimestampSecond(42L), randomKey().publicKey, Color(42, 42, 42), "ACINQ", Nil)
val remoteNode = Router.PublicNode(ann, 7, 561_000 sat)
eclair.node(ann.nodeId).pipeTo(sender.ref)
assert(router.expectMsgType[Router.GetNode].nodeId == ann.nodeId)
router.reply(remoteNode)
sender.expectMsg(Some(remoteNode))

val unknownNode = Router.UnknownNode(randomKey().publicKey)
eclair.node(unknownNode.nodeId).pipeTo(sender.ref)
assert(router.expectMsgType[Router.GetNode].nodeId == unknownNode.nodeId)
router.reply(unknownNode)
sender.expectMsg(None)
}

test("return node announcements") { f =>
import f._

Expand Down
100 changes: 55 additions & 45 deletions eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,50 @@ import scala.concurrent.duration._

class RouterSpec extends BaseRouterSpec {

test("properly announce valid new nodes announcements and ignore invalid ones") { fixture =>
import fixture._
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent])
system.eventStream.subscribe(eventListener.ref, classOf[Rebroadcast])
val peerConnection = TestProbe()

{
// continue to rebroadcast node updates with deprecated Torv2 addresses
val torv2Address = List(NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get)
val node_c_torv2 = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), torv2Address, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_torv2))
peerConnection.expectMsg(TransportHandler.ReadAck(node_c_torv2))
peerConnection.expectMsg(GossipDecision.Accepted(node_c_torv2))
eventListener.expectMsg(NodeUpdated(node_c_torv2))
router ! Router.TickBroadcast
val rebroadcast = eventListener.expectMsgType[Rebroadcast]
assert(rebroadcast.nodes.contains(node_c_torv2))
}
{
// rebroadcast node updates with a single DNS hostname addresses
val hostname = List(NodeAddress.fromParts("acinq.co", 9735).get)
val node_c_hostname = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), hostname, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 10)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_hostname))
peerConnection.expectMsg(TransportHandler.ReadAck(node_c_hostname))
peerConnection.expectMsg(GossipDecision.Accepted(node_c_hostname))
eventListener.expectMsg(NodeUpdated(node_c_hostname))
router ! Router.TickBroadcast
val rebroadcast = eventListener.expectMsgType[Rebroadcast]
assert(rebroadcast.nodes.contains(node_c_hostname))
}
{
// do NOT rebroadcast node updates with more than one DNS hostname addresses
val multiHostnames = List(NodeAddress.fromParts("acinq.co", 9735).get, NodeAddress.fromParts("acinq.fr", 9735).get)
val node_c_noForward = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), multiHostnames, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 20)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_noForward))
peerConnection.expectMsg(TransportHandler.ReadAck(node_c_noForward))
peerConnection.expectMsg(GossipDecision.Accepted(node_c_noForward))
eventListener.expectMsg(NodeUpdated(node_c_noForward))
router ! Router.TickBroadcast
eventListener.expectNoMessage(100 millis)
}
}

test("properly announce valid new channels and ignore invalid ones") { fixture =>
import fixture._
val eventListener = TestProbe()
Expand Down Expand Up @@ -262,6 +306,17 @@ class RouterSpec extends BaseRouterSpec {
watcher.expectNoMessage(100 millis)
}

test("get nodes") { fixture =>
import fixture._

val probe = TestProbe()
val unknownNodeId = randomKey().publicKey
probe.send(router, GetNode(unknownNodeId))
probe.expectMsg(UnknownNode(unknownNodeId))
probe.send(router, GetNode(b))
probe.expectMsg(PublicNode(node_b, 2, publicChannelCapacity * 2))
}

test("properly announce lost channels and nodes") { fixture =>
import fixture._
val eventListener = TestProbe()
Expand Down Expand Up @@ -1040,49 +1095,4 @@ class RouterSpec extends BaseRouterSpec {
}
}

test("properly announce valid new nodes announcements and ignore invalid ones") { fixture =>
Comment thread
t-bast marked this conversation as resolved.
import fixture._
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent])
system.eventStream.subscribe(eventListener.ref, classOf[Rebroadcast])
val peerConnection = TestProbe()

{
// continue to rebroadcast node updates with deprecated Torv2 addresses
val torv2Address = List(NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get)
val node_c_torv2 = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), torv2Address, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_torv2))
peerConnection.expectMsg(TransportHandler.ReadAck(node_c_torv2))
peerConnection.expectMsg(GossipDecision.Accepted(node_c_torv2))
eventListener.expectMsg(NodeUpdated(node_c_torv2))
router ! Router.TickBroadcast
val rebroadcast = eventListener.expectMsgType[Rebroadcast]
assert(rebroadcast.nodes.contains(node_c_torv2))
}

{
// rebroadcast node updates with a single DNS hostname addresses
val hostname = List(NodeAddress.fromParts("acinq.co", 9735).get)
val node_c_hostname = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), hostname, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 10)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_hostname))
peerConnection.expectMsg(TransportHandler.ReadAck(node_c_hostname))
peerConnection.expectMsg(GossipDecision.Accepted(node_c_hostname))
eventListener.expectMsg(NodeUpdated(node_c_hostname))
router ! Router.TickBroadcast
val rebroadcast = eventListener.expectMsgType[Rebroadcast]
assert(rebroadcast.nodes.contains(node_c_hostname))
}

{
// do NOT rebroadcast node updates with more than one DNS hostname addresses
val multiHostnames = List(NodeAddress.fromParts("acinq.co", 9735).get, NodeAddress.fromParts("acinq.fr", 9735).get)
val node_c_noForward = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), multiHostnames, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 20)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_noForward))
peerConnection.expectMsg(TransportHandler.ReadAck(node_c_noForward))
peerConnection.expectMsg(GossipDecision.Accepted(node_c_noForward))
eventListener.expectMsg(NodeUpdated(node_c_noForward))
router ! Router.TickBroadcast
eventListener.expectNoMessage(100 millis)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@ trait PathFinding {
}
}

val node: Route = postRequest("node") { implicit t =>
Comment thread
t-bast marked this conversation as resolved.
formFields(nodeIdFormParam) { nodeId =>
completeOrNotFound(eclairApi.node(nodeId))
}
}

val nodes: Route = postRequest("nodes") { implicit t =>
formFields(nodeIdsFormParam.?) { nodeIds_opt =>
complete(eclairApi.nodes(nodeIds_opt.map(_.toSet)))
}
}

val pathFindingRoutes: Route = findRoute ~ findRouteToNode ~ findRouteBetweenNodes ~ nodes
val pathFindingRoutes: Route = findRoute ~ findRouteToNode ~ findRouteBetweenNodes ~ node ~ nodes

}