Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor codecs and onion types #2057

Closed
wants to merge 3 commits into from
Closed
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
4 changes: 4 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Node operators should watch this file very regularly.
An event is also sent to the event stream for every such notification.
This lets plugins notify the node operator via external systems (push notifications, email, etc).

### Initial support for onion messages

Eclair now supports the feature `option_onion_messages`. If this feature is enabled, eclair will relay onion messages, initiating or receiving onion messages is not supported yet.

### API changes

#### Timestamps
Expand Down
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ eclair {
option_anchor_outputs = disabled
option_anchors_zero_fee_htlc_tx = disabled
option_shutdown_anysegwit = optional
option_onion_messages = disabled
trampoline_payment = disabled
keysend = disabled
}
Expand Down
6 changes: 6 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ object Features {
val mandatory = 26
}

case object OnionMessages extends Feature {
val rfcName = "option_onion_messages"
val mandatory = 38
}

// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
// We're not advertising these bits yet in our announcements, clients have to assume support.
// This is why we haven't added them yet to `areSupported`.
Expand Down Expand Up @@ -231,6 +236,7 @@ object Features {
AnchorOutputs,
AnchorOutputsZeroFeeHtlcTx,
ShutdownAnySegwit,
OnionMessages,
KeySend
)

Expand Down
28 changes: 16 additions & 12 deletions eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,12 @@ object Sphinx extends Logging {
* failure messages upstream.
* or a BadOnion error containing the hash of the invalid onion.
*/
def peel(privateKey: PrivateKey, associatedData: ByteVector, packet: OnionRoutingPacket): Either[BadOnion, DecryptedPacket] = packet.version match {
def peel(privateKey: PrivateKey, associatedData: Option[ByteVector32], packet: OnionRoutingPacket): Either[BadOnion, DecryptedPacket] = packet.version match {
case 0 => Try(PublicKey(packet.publicKey, checkValid = true)) match {
case Success(packetEphKey) =>
val sharedSecret = computeSharedSecret(packetEphKey, privateKey)
val mu = generateKey("mu", sharedSecret)
val check = mac(mu, packet.payload ++ associatedData)
val check = mac(mu, associatedData.map(packet.payload ++ _).getOrElse(packet.payload))
if (check == packet.hmac) {
val rho = generateKey("rho", sharedSecret)
// Since we don't know the length of the per-hop payload (we will learn it once we decode the first bytes),
Expand Down Expand Up @@ -208,7 +208,7 @@ object Sphinx extends Logging {
* @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet.
* @return the next packet.
*/
def wrap(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Either[ByteVector, OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): OnionRoutingPacket = {
def wrap(payload: ByteVector, associatedData: Option[ByteVector32], ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Either[ByteVector, OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): OnionRoutingPacket = {
val packetPayloadLength = packet match {
case Left(startingBytes) => startingBytes.length.toInt
case Right(p) => p.payload.length.toInt
Expand All @@ -226,7 +226,7 @@ object Sphinx extends Logging {
onionPayload2.dropRight(onionPayloadFiller.length) ++ onionPayloadFiller
}

val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData)
val nextHmac = mac(generateKey("mu", sharedSecret), associatedData.map(nextOnionPayload ++ _).getOrElse(nextOnionPayload))
val nextPacket = OnionRoutingPacket(Version, ephemeralPublicKey.value, nextOnionPayload, nextHmac)
nextPacket
}
Expand All @@ -242,7 +242,7 @@ object Sphinx extends Logging {
* @return An onion packet with all shared secrets. The onion packet can be sent to the first node in the list, and
* the shared secrets (one per node) can be used to parse returned failure messages if needed.
*/
def create(sessionKey: PrivateKey, packetPayloadLength: Int, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = {
def create(sessionKey: PrivateKey, packetPayloadLength: Int, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: Option[ByteVector32]): PacketAndSecrets = {
require(payloads.map(_.length + MacLength).sum <= packetPayloadLength, s"packet per-hop payloads cannot exceed $packetPayloadLength bytes")
val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys)
val filler = generateFiller("rho", packetPayloadLength, sharedsecrets.dropRight(1), payloads.dropRight(1))
Expand Down Expand Up @@ -374,12 +374,17 @@ object Sphinx extends Logging {
case class BlindedNode(blindedPublicKey: PublicKey, encryptedPayload: ByteVector)

/**
* @param introductionNode the first node should not be blinded, otherwise the sender cannot locate it.
* @param blindedNodes blinded nodes (not including the introduction node).
* @param introductionNodeId the first node, not blinded so that the sender can locate it.
* @param blindingKey blinding tweak that can be used by the introduction node to derive the private key that
* matches the blinded public key.
* @param blindedNodes blinded nodes (including the introduction node).
*/
case class BlindedRoute(introductionNode: IntroductionNode, blindedNodes: Seq[BlindedNode]) {
val nodeIds: Seq[PublicKey] = introductionNode.publicKey +: blindedNodes.map(_.blindedPublicKey)
val encryptedPayloads: Seq[ByteVector] = introductionNode.encryptedPayload +: blindedNodes.map(_.encryptedPayload)
case class BlindedRoute(introductionNodeId: PublicKey, blindingKey: PublicKey, blindedNodes: Seq[BlindedNode]) {
require(blindedNodes.nonEmpty, "blinded route must not be empty")
val introductionNode: IntroductionNode = IntroductionNode(introductionNodeId, blindedNodes.head.blindedPublicKey, blindingKey, blindedNodes.head.encryptedPayload)
val subsequentNodes: Seq[BlindedNode] = blindedNodes.tail
val blindedNodeIds: Seq[PublicKey] = blindedNodes.map(_.blindedPublicKey)
val encryptedPayloads: Seq[ByteVector] = blindedNodes.map(_.encryptedPayload)
}

/**
Expand All @@ -402,8 +407,7 @@ object Sphinx extends Logging {
e = e.multiply(PrivateKey(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes)))
(BlindedNode(blindedPublicKey, encryptedPayload ++ mac), blindingKey)
}.unzip
val introductionNode = IntroductionNode(publicKeys.head, blindedHops.head.blindedPublicKey, blindingKeys.head, blindedHops.head.encryptedPayload)
BlindedRoute(introductionNode, blindedHops.tail)
BlindedRoute(publicKeys.head, blindingKeys.head, blindedHops)
}

/**
Expand Down
43 changes: 39 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ import fr.acinq.eclair.blockchain.{OnChainAddressGenerator, OnChainChannelFunder
import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Monitoring.Metrics
import fr.acinq.eclair.io.PeerConnection.KillReason
import fr.acinq.eclair.message.OnionMessages
import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
import fr.acinq.eclair.wire.protocol
import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, RoutingMessage, UnknownMessage, Warning}
import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, OnionMessage, RoutingMessage, UnknownMessage, Warning}
import scodec.bits.ByteVector

import java.net.InetSocketAddress
Expand Down Expand Up @@ -65,7 +66,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
FinalChannelId(state.channelId) -> channel
}.toMap

goto(DISCONNECTED) using DisconnectedData(channels) // when we restart, we will attempt to reconnect right away, but then we'll wait
goto(DISCONNECTED) using DisconnectedData(channels, None) // when we restart, we will attempt to reconnect right away, but then we'll wait
}

when(DISCONNECTED) {
Expand All @@ -74,6 +75,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
stay()

case Event(connectionReady: PeerConnection.ConnectionReady, d: DisconnectedData) =>
d.messageToRelay.foreach(msg => self ! Peer.SendOnionMessage(remoteNodeId, msg))
gotoConnected(connectionReady, d.channels.map { case (k: ChannelId, v) => (k, v) })

case Event(Terminated(actor), d: DisconnectedData) if d.channels.exists(_._2 == actor) =>
Expand All @@ -94,6 +96,19 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
stay() using d.copy(channels = d.channels + (FinalChannelId(channelId) -> channel))

case Event(_: LightningMessage, _) => stay() // we probably just got disconnected and that's the last messages we received

case Event(Peer.SendOnionMessage(toNodeId, msg), d: DisconnectedData) if toNodeId == remoteNodeId =>
// We may drop a previous messageToRelay but that's fine. If we receive several messages to relay while trying to connect, we're probably getting spammed.
stay() using d.copy(messageToRelay = Some(msg))

case Event(_ : PeerConnection.ConnectionResult.Failure, d: DisconnectedData) =>
if (d.channels.isEmpty) {
// we don't have any channel with this peer and we can't connect to it so we just drop it
stopPeer()
} else {
stay() using d.copy(messageToRelay = None)
}

}

when(CONNECTED) {
Expand Down Expand Up @@ -223,7 +238,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
stopPeer()
} else {
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.channels.collect { case (k: FinalChannelId, v) => (k, v) })
goto(DISCONNECTED) using DisconnectedData(d.channels.collect { case (k: FinalChannelId, v) => (k, v) }, None)
}

case Event(Terminated(actor), d: ConnectedData) if d.channels.values.toSet.contains(actor) =>
Expand All @@ -243,6 +258,20 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
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)
gotoConnected(connectionReady, d.channels)

case Event(msg: OnionMessage, _: ConnectedData) =>
if (nodeParams.features.hasFeature(Features.OnionMessages)) {
OnionMessages.process(nodeParams.privateKey, msg) match {
case OnionMessages.DropMessage(_) => () // We ignore bad messages
case OnionMessages.RelayMessage(nextNodeId, dataToRelay) => context.parent ! Peer.SendOnionMessage(nextNodeId, dataToRelay)
case OnionMessages.ReceiveMessage(_, _) => () // We only relay messages
}
}
stay()

case Event(Peer.SendOnionMessage(toNodeId, msg), d: ConnectedData) if toNodeId == remoteNodeId =>
d.peerConnection ! msg
stay()

case Event(unknownMsg: UnknownMessage, d: ConnectedData) if nodeParams.pluginMessageTags.contains(unknownMsg.tag) =>
context.system.eventStream.publish(UnknownMessageReceived(self, remoteNodeId, unknownMsg, d.connectionInfo))
stay()
Expand Down Expand Up @@ -407,10 +436,14 @@ object Peer {
def channels: Map[_ <: ChannelId, ActorRef] // will be overridden by Map[FinalChannelId, ActorRef] or Map[ChannelId, ActorRef]
}
case object Nothing extends Data { override def channels = Map.empty }
case class DisconnectedData(channels: Map[FinalChannelId, ActorRef]) extends Data

case class DisconnectedData(channels: Map[FinalChannelId, ActorRef], messageToRelay: Option[OnionMessage]) extends Data

case class ConnectedData(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data {
val connectionInfo: ConnectionInfo = ConnectionInfo(address, peerConnection, localInit, remoteInit)

def localFeatures: Features = localInit.features

def remoteFeatures: Features = remoteInit.features
}

Expand All @@ -427,6 +460,8 @@ object Peer {
def apply(uri: NodeURI): Connect = new Connect(uri.nodeId, Some(uri.address))
}

case class SendOnionMessage(nodeId: PublicKey, message: OnionMessage) extends PossiblyHarmful

case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful
case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful {
require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory)
case None => sender() ! Status.Failure(new RuntimeException("peer not found"))
}

case s@Peer.SendOnionMessage(nodeId, _) =>
val peer = createOrGetPeer(nodeId, offlineChannels = Set.empty)
peer.tell(Peer.Connect(nodeId, None), peer)
peer forward s

case o: Peer.OpenChannel =>
getPeer(o.remoteNodeId) match {
case Some(peer) => peer forward o
Expand Down
Loading