@@ -28,7 +28,6 @@ import scodec.{Attempt, DecodeResult}
28
28
29
29
import scala .annotation .tailrec
30
30
import scala .concurrent .duration .FiniteDuration
31
- import scala .util .Try
32
31
33
32
object OnionMessages {
34
33
@@ -42,35 +41,55 @@ object OnionMessages {
42
41
case class Recipient (nodeId : PublicKey , pathId : Option [ByteVector ], padding : Option [ByteVector ] = None ) extends Destination
43
42
// @formatter:on
44
43
45
- def buildRoute (blindingSecret : PrivateKey ,
46
- intermediateNodes : Seq [IntermediateNode ],
47
- destination : Destination ): Sphinx .RouteBlinding .BlindedRoute = {
48
- val last = destination match {
49
- case Recipient (nodeId, _, _) => OutgoingNodeId (nodeId) :: Nil
50
- case BlindedPath (Sphinx .RouteBlinding .BlindedRoute (nodeId, blindingKey, _)) => OutgoingNodeId (nodeId) :: NextBlinding (blindingKey) :: Nil
51
- }
52
- val intermediatePayloads = if (intermediateNodes.isEmpty) {
44
+ private def buildIntermediatePayloads (intermediateNodes : Seq [IntermediateNode ], nextTlvs : Set [RouteBlindingEncryptedDataTlv ]): Seq [ByteVector ] = {
45
+ if (intermediateNodes.isEmpty) {
53
46
Nil
54
47
} else {
55
- (intermediateNodes.tail.map(node => Set (OutgoingNodeId (node.nodeId))) :+ last )
48
+ (intermediateNodes.tail.map(node => Set (OutgoingNodeId (node.nodeId))) :+ nextTlvs )
56
49
.zip(intermediateNodes).map { case (tlvs, hop) => hop.padding.map(Padding ).toSet[RouteBlindingEncryptedDataTlv ] ++ tlvs }
57
50
.map(tlvs => RouteBlindingEncryptedDataCodecs .blindedRouteDataCodec.encode(TlvStream (tlvs)).require.bytes)
58
51
}
52
+ }
53
+
54
+ def buildRoute (blindingSecret : PrivateKey ,
55
+ intermediateNodes : Seq [IntermediateNode ],
56
+ recipient : Recipient ): Sphinx .RouteBlinding .BlindedRoute = {
57
+ val intermediatePayloads = buildIntermediatePayloads(intermediateNodes, Set (OutgoingNodeId (recipient.nodeId)))
58
+ val tlvs : Set [RouteBlindingEncryptedDataTlv ] = Set (recipient.padding.map(Padding ), recipient.pathId.map(PathId )).flatten
59
+ val lastPayload = RouteBlindingEncryptedDataCodecs .blindedRouteDataCodec.encode(TlvStream (tlvs)).require.bytes
60
+ Sphinx .RouteBlinding .create(blindingSecret, intermediateNodes.map(_.nodeId) :+ recipient.nodeId, intermediatePayloads :+ lastPayload).route
61
+ }
62
+
63
+ private def buildRouteFrom (originKey : PrivateKey ,
64
+ blindingSecret : PrivateKey ,
65
+ intermediateNodes : Seq [IntermediateNode ],
66
+ destination : Destination ): Option [Sphinx .RouteBlinding .BlindedRoute ] = {
59
67
destination match {
60
- case Recipient (nodeId, pathId, padding) =>
61
- val tlvs : Set [ RouteBlindingEncryptedDataTlv ] = Set (padding.map( Padding ), pathId.map( PathId )).flatten
62
- val lastPayload = RouteBlindingEncryptedDataCodecs .blindedRouteDataCodec.encode( TlvStream (tlvs)).require.bytes
63
- Sphinx . RouteBlinding .create(blindingSecret, intermediateNodes.map(_.nodeId) :+ nodeId, intermediatePayloads :+ lastPayload).route
64
- case BlindedPath (route ) =>
65
- if (intermediateNodes.isEmpty) {
66
- route
67
- } else {
68
- val routePrefix = Sphinx .RouteBlinding .create(blindingSecret, intermediateNodes.map(_.nodeId), intermediatePayloads). route
69
- Sphinx . RouteBlinding . BlindedRoute (routePrefix.introductionNodeId, routePrefix.blindingKey, routePrefix.blindedNodes ++ route.blindedNodes)
68
+ case recipient : Recipient => Some (buildRoute(blindingSecret, intermediateNodes, recipient))
69
+ case BlindedPath (route) if route.introductionNodeId == originKey.publicKey =>
70
+ RouteBlindingEncryptedDataCodecs .decode(originKey, route.blindingKey, route.blindedNodes.head.encryptedPayload) match {
71
+ case Left (_) => None
72
+ case Right (decoded ) =>
73
+ decoded.tlvs.get[ RouteBlindingEncryptedDataTlv . OutgoingNodeId ] match {
74
+ case None => None
75
+ case Some ( RouteBlindingEncryptedDataTlv . OutgoingNodeId (nextNodeId)) =>
76
+ Some ( Sphinx .RouteBlinding .BlindedRoute (nextNodeId, decoded.nextBlinding, route.blindedNodes.tail))
77
+ }
70
78
}
79
+ case BlindedPath (route) if intermediateNodes.isEmpty => Some (route)
80
+ case BlindedPath (route) =>
81
+ val intermediatePayloads = buildIntermediatePayloads(intermediateNodes, Set (OutgoingNodeId (route.introductionNodeId), NextBlinding (route.blindingKey)))
82
+ val routePrefix = Sphinx .RouteBlinding .create(blindingSecret, intermediateNodes.map(_.nodeId), intermediatePayloads).route
83
+ Some (Sphinx .RouteBlinding .BlindedRoute (routePrefix.introductionNodeId, routePrefix.blindingKey, routePrefix.blindedNodes ++ route.blindedNodes))
71
84
}
72
85
}
73
86
87
+ // @formatter:off
88
+ sealed trait BuildMessageError
89
+ case class MessageTooLarge (payloadSize : Long ) extends BuildMessageError
90
+ case class InvalidDestination (destination : Destination ) extends BuildMessageError
91
+ // @formatter:on
92
+
74
93
/**
75
94
* Builds an encrypted onion containing a message that should be relayed to the destination.
76
95
*
@@ -81,28 +100,32 @@ object OnionMessages {
81
100
* @param content List of TLVs to send to the recipient of the message
82
101
* @return The node id to send the onion to and the onion containing the message
83
102
*/
84
- def buildMessage (sessionKey : PrivateKey ,
103
+ def buildMessage (nodeKey : PrivateKey ,
104
+ sessionKey : PrivateKey ,
85
105
blindingSecret : PrivateKey ,
86
106
intermediateNodes : Seq [IntermediateNode ],
87
107
destination : Destination ,
88
- content : TlvStream [OnionMessagePayloadTlv ]): Try [(PublicKey , OnionMessage )] = Try {
89
- val route = buildRoute(blindingSecret, intermediateNodes, destination)
90
- val lastPayload = MessageOnionCodecs .perHopPayloadCodec.encode(TlvStream (content.records + EncryptedData (route.encryptedPayloads.last), content.unknown)).require.bytes
91
- val payloads = route.encryptedPayloads.dropRight(1 ).map(encTlv => MessageOnionCodecs .perHopPayloadCodec.encode(TlvStream (EncryptedData (encTlv))).require.bytes) :+ lastPayload
92
- val payloadSize = payloads.map(_.length + Sphinx .MacLength ).sum
93
- val packetSize = if (payloadSize <= 1300 ) {
94
- 1300
95
- } else if (payloadSize <= 32768 ) {
96
- 32768
97
- } else if (payloadSize > 65432 ) {
98
- // A payload of size 65432 corresponds to a total lightning message size of 65535.
99
- throw new Exception (s " Message is too large: payloadSize= $payloadSize" )
100
- } else {
101
- payloadSize.toInt
108
+ content : TlvStream [OnionMessagePayloadTlv ]): Either [BuildMessageError , (PublicKey , OnionMessage )] = {
109
+ buildRouteFrom(nodeKey, blindingSecret, intermediateNodes, destination) match {
110
+ case None => Left (InvalidDestination (destination))
111
+ case Some (route) =>
112
+ val lastPayload = MessageOnionCodecs .perHopPayloadCodec.encode(TlvStream (content.records + EncryptedData (route.encryptedPayloads.last), content.unknown)).require.bytes
113
+ val payloads = route.encryptedPayloads.dropRight(1 ).map(encTlv => MessageOnionCodecs .perHopPayloadCodec.encode(TlvStream (EncryptedData (encTlv))).require.bytes) :+ lastPayload
114
+ val payloadSize = payloads.map(_.length + Sphinx .MacLength ).sum
115
+ val packetSize = if (payloadSize <= 1300 ) {
116
+ 1300
117
+ } else if (payloadSize <= 32768 ) {
118
+ 32768
119
+ } else if (payloadSize > 65432 ) {
120
+ // A payload of size 65432 corresponds to a total lightning message size of 65535.
121
+ return Left (MessageTooLarge (payloadSize))
122
+ } else {
123
+ payloadSize.toInt
124
+ }
125
+ // Since we are setting the packet size based on the payload, the onion creation should never fail (hence the `.get`).
126
+ val Sphinx .PacketAndSecrets (packet, _) = Sphinx .create(sessionKey, packetSize, route.blindedNodes.map(_.blindedPublicKey), payloads, None ).get
127
+ Right ((route.introductionNodeId, OnionMessage (route.blindingKey, packet)))
102
128
}
103
- // Since we are setting the packet size based on the payload, the onion creation should never fail (hence the `.get`).
104
- val Sphinx .PacketAndSecrets (packet, _) = Sphinx .create(sessionKey, packetSize, route.blindedNodes.map(_.blindedPublicKey), payloads, None ).get
105
- (route.introductionNodeId, OnionMessage (route.blindingKey, packet))
106
129
}
107
130
108
131
// @formatter:off
0 commit comments