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

NodeId #601

Merged
merged 3 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
41 changes: 41 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package fr.acinq.lightning

import fr.acinq.bitcoin.PublicKey
import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.Output
import fr.acinq.lightning.wire.LightningCodecs

sealed interface NodeId {
t-bast marked this conversation as resolved.
Show resolved Hide resolved
fun write(out: Output)

data class Standard(val publicKey: PublicKey) : NodeId {
override fun write(out: Output) {
LightningCodecs.writeBytes(publicKey.value, out)
}
}

data class ShortChannelIdDir(val isNode1: Boolean, val scid: ShortChannelId) : NodeId {
override fun write(out: Output) {
LightningCodecs.writeByte(if (isNode1) 0 else 1, out)
LightningCodecs.writeInt64(scid.toLong(), out)
}
}

companion object {
fun read(input: Input): NodeId {
val firstByte = LightningCodecs.byte(input)
if (firstByte == 0 || firstByte == 1) {
val isNode1 = firstByte == 0
val scid = ShortChannelId(LightningCodecs.int64(input))
return ShortChannelIdDir(isNode1, scid)
} else if (firstByte == 2 || firstByte == 3) {
val publicKey = PublicKey(ByteArray(1) { firstByte.toByte() } + LightningCodecs.bytes(input, 32))
return Standard(publicKey)
} else {
throw IllegalArgumentException("unexpected first byte: $firstByte")
}
}

operator fun invoke(publicKey: PublicKey): NodeId = Standard(publicKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import fr.acinq.bitcoin.ByteVector
import fr.acinq.bitcoin.Crypto
import fr.acinq.bitcoin.PrivateKey
import fr.acinq.bitcoin.PublicKey
import fr.acinq.lightning.NodeId
import fr.acinq.lightning.crypto.sphinx.Sphinx

object RouteBlinding {

/**
* @param publicKey introduction node's public key (which cannot be blinded since the sender need to find a route to it).
* @param nodeId introduction node's id (which cannot be blinded since the sender need to find a route to it).
* @param blindedPublicKey blinded public key, which hides the real public key.
* @param blindingEphemeralKey blinding tweak that can be used by the receiving node to derive the private key that
* matches the blinded public key.
* @param encryptedPayload encrypted payload that can be decrypted with the introduction node's private key and the
* blinding ephemeral key.
*/
data class IntroductionNode(
val publicKey: PublicKey,
val nodeId: NodeId,
val blindedPublicKey: PublicKey,
val blindingEphemeralKey: PublicKey,
val encryptedPayload: ByteVector
Expand All @@ -37,7 +38,7 @@ object RouteBlinding {
* @param blindedNodes blinded nodes (including the introduction node).
*/
data class BlindedRoute(
val introductionNodeId: PublicKey,
val introductionNodeId: NodeId,
val blindingKey: PublicKey,
val blindedNodes: List<BlindedNode>
) {
Expand Down Expand Up @@ -78,7 +79,7 @@ object RouteBlinding {
e *= PrivateKey(Crypto.sha256(blindingKey.value.toByteArray() + sharedSecret.toByteArray()))
Pair(BlindedNode(blindedPublicKey, ByteVector(encryptedPayload + mac)), blindingKey)
}.unzip()
return BlindedRoute(publicKeys.first(), blindingKeys.first(), blindedHops)
return BlindedRoute(NodeId(publicKeys.first()), blindingKeys.first(), blindedHops)
}

/**
Expand Down
26 changes: 13 additions & 13 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.io.ByteArrayOutput
import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.Output
import fr.acinq.lightning.NodeId
import fr.acinq.lightning.crypto.RouteBlinding


sealed class OnionMessagePayloadTlv : Tlv {
sealed interface OnionMessagePayloadTlv : Tlv {
t-bast marked this conversation as resolved.
Show resolved Hide resolved
/**
* Onion messages may provide a reply path, allowing the recipient to send a message back to the original sender.
* The reply path uses route blinding, which ensures that the sender doesn't leak its identity to the recipient.
*/
data class ReplyPath(val blindedRoute: RouteBlinding.BlindedRoute) : OnionMessagePayloadTlv() {
data class ReplyPath(val blindedRoute: RouteBlinding.BlindedRoute) : OnionMessagePayloadTlv {
override val tag: Long get() = ReplyPath.tag
override fun write(out: Output) {
LightningCodecs.writeBytes(blindedRoute.introductionNodeId.value, out)
blindedRoute.introductionNodeId.write(out)
LightningCodecs.writeBytes(blindedRoute.blindingKey.value, out)
LightningCodecs.writeByte(blindedRoute.blindedNodes.size, out)
for (hop in blindedRoute.blindedNodes) {
LightningCodecs.writeBytes(hop.blindedPublicKey.value, out)
LightningCodecs.writeU16(hop.encryptedPayload.size(), out)
Expand All @@ -29,15 +30,14 @@ sealed class OnionMessagePayloadTlv : Tlv {
companion object : TlvValueReader<ReplyPath> {
const val tag: Long = 2
override fun read(input: Input): ReplyPath {
val firstNodeId = PublicKey(LightningCodecs.bytes(input, 33))
val firstNodeId = NodeId.read(input)
val blinding = PublicKey(LightningCodecs.bytes(input, 33))
val path = sequence {
while (input.availableBytes > 0) {
val blindedPublicKey = PublicKey(LightningCodecs.bytes(input, 33))
val encryptedPayload = ByteVector(LightningCodecs.bytes(input, LightningCodecs.u16(input)))
yield(RouteBlinding.BlindedNode(blindedPublicKey, encryptedPayload))
}
}.toList()
val numHops = LightningCodecs.byte(input)
val path = (0 until numHops).map {
val blindedPublicKey = PublicKey(LightningCodecs.bytes(input, 33))
val encryptedPayload = ByteVector(LightningCodecs.bytes(input, LightningCodecs.u16(input)))
RouteBlinding.BlindedNode(blindedPublicKey, encryptedPayload)
}
return ReplyPath(RouteBlinding.BlindedRoute(firstNodeId, blinding, path))
}
}
Expand All @@ -48,7 +48,7 @@ sealed class OnionMessagePayloadTlv : Tlv {
* This ensures that intermediate nodes can't know whether they're forwarding a message or its reply.
* The sender must provide some encrypted data for each intermediate node which lets them locate the next node.
*/
data class EncryptedData(val data: ByteVector) : OnionMessagePayloadTlv() {
data class EncryptedData(val data: ByteVector) : OnionMessagePayloadTlv {
override val tag: Long get() = EncryptedData.tag
override fun write(out: Output) = LightningCodecs.writeBytes(data, out)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.io.ByteArrayOutput
import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.Output
import fr.acinq.lightning.NodeId


sealed class RouteBlindingEncryptedDataTlv : Tlv {
Expand All @@ -22,14 +23,14 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv {
}

/** Id of the next node. */
data class OutgoingNodeId(val nodeId: PublicKey) : RouteBlindingEncryptedDataTlv() {
data class OutgoingNodeId(val nodeId: NodeId) : RouteBlindingEncryptedDataTlv() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're modifying the official spec TLV here (tag = 4). We need to get agreement on this change at the spec level for that, otherwise we may have painful compatibility issues to handle in the future...I believe that changing the outgoing_node_id TLV to use this new sciddir_or_pubkey makes a lot of sense and should be in the BOLTs, don't you? This is an opportunity to create a small spec PR based on master that:

  • extracts the sciddir_or_pubkey from the offers spec PR
  • uses it in the outgoing_node_id TLV field of onion messages / blinded paths
  • adds or modifies a test vector to use this

I expect that this spec PR would be accepted somewhat quickly, and it would make the offers spec PR slightly smaller in scope. It's also important to create that spec PR before people actually start using offers, this way we can just change the TLV without caring about backwards-compat.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can merge that code to both lightning-kmp and eclair though, even before the spec PR gets accepted, because:

  • until we ship bolt 12 support in phoenix, we actually will never create such tlv fields
  • on the eclair side, we're just adding support for reading this new case, not writing it, so it won't impact other nodes and we can update the code without caring about backwards-compat

But we should still create a spec PR to get wide support for this feature!

override val tag: Long get() = OutgoingNodeId.tag
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)
override fun write(out: Output) = nodeId.write(out)

companion object : TlvValueReader<OutgoingNodeId> {
const val tag: Long = 4
override fun read(input: Input): OutgoingNodeId =
OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
OutgoingNodeId(NodeId.read(input))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fr.acinq.bitcoin.ByteVector
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.PrivateKey
import fr.acinq.bitcoin.PublicKey
import fr.acinq.lightning.NodeId
import fr.acinq.lightning.crypto.RouteBlinding
import fr.acinq.lightning.crypto.sphinx.Sphinx.computeEphemeralPublicKeysAndSharedSecrets
import fr.acinq.lightning.crypto.sphinx.Sphinx.decodePayloadLength
Expand Down Expand Up @@ -562,8 +563,8 @@ class SphinxTestsCommon : LightningTestSuite() {
fun `create blinded route -- reference test vector`() {
val sessionKey = PrivateKey(ByteVector32("0101010101010101010101010101010101010101010101010101010101010101"))
val blindedRoute = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads)
assertEquals(blindedRoute.introductionNode.publicKey, publicKeys[0])
assertEquals(blindedRoute.introductionNodeId, publicKeys[0])
assertEquals(blindedRoute.introductionNode.nodeId, NodeId(publicKeys[0]))
assertEquals(blindedRoute.introductionNodeId, NodeId(publicKeys[0]))
assertEquals(blindedRoute.introductionNode.blindedPublicKey, PublicKey.fromHex("02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2"))
assertEquals(blindedRoute.introductionNode.blindingEphemeralKey, PublicKey.fromHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))
assertEquals(blindedRoute.introductionNode.encryptedPayload, ByteVector("af4fbf67bd52520bdfab6a88cd4e7f22ffad08d8b153b17ff303f93fdb4712"))
Expand Down Expand Up @@ -641,7 +642,7 @@ class SphinxTestsCommon : LightningTestSuite() {
)
Pair(RouteBlinding.create(sessionKey, publicKeys.take(2), payloads), payloads)
}
val blindedRoute = RouteBlinding.BlindedRoute(publicKeys[0], blindedRouteStart.blindingKey, blindedRouteStart.blindedNodes + blindedRouteEnd.blindedNodes)
val blindedRoute = RouteBlinding.BlindedRoute(NodeId(publicKeys[0]), blindedRouteStart.blindingKey, blindedRouteStart.blindedNodes + blindedRouteEnd.blindedNodes)
assertEquals(blindedRoute.blindingKey, PublicKey.fromHex("024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"))
assertEquals(blindedRoute.blindedNodeIds, listOf(
PublicKey.fromHex("0303176d13958a8a59d59517a6223e12cf291ba5f65c8011efcdca0a52c3850abc"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fr.acinq.lightning.wire

import fr.acinq.bitcoin.ByteVector
import fr.acinq.bitcoin.PublicKey
import fr.acinq.lightning.NodeId
import fr.acinq.lightning.tests.utils.LightningTestSuite
import fr.acinq.lightning.wire.RouteBlindingEncryptedDataTlv.*
import kotlin.test.Test
Expand All @@ -14,7 +15,7 @@ class RouteBlindingTestsCommon : LightningTestSuite() {
ByteVector("01080000000000000000 042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145") to RouteBlindingEncryptedData(
TlvStream(
Padding(ByteVector("0000000000000000")),
OutgoingNodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145")))
OutgoingNodeId(NodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"))))
)
),
ByteVector("0109000000000000000000 06204242424242424242424242424242424242424242424242424242424242424242") to RouteBlindingEncryptedData(
Expand All @@ -24,10 +25,10 @@ class RouteBlindingTestsCommon : LightningTestSuite() {
)
),
ByteVector("0421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991") to RouteBlindingEncryptedData(
TlvStream(OutgoingNodeId(PublicKey(ByteVector("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"))))
TlvStream(OutgoingNodeId(NodeId(PublicKey(ByteVector("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991")))))
),
ByteVector("042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145") to RouteBlindingEncryptedData(
TlvStream(OutgoingNodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"))))
TlvStream(OutgoingNodeId(NodeId(PublicKey(ByteVector("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145")))))
),
ByteVector("010f000000000000000000000000000000 061000112233445566778899aabbccddeeff") to RouteBlindingEncryptedData(
TlvStream(
Expand All @@ -38,12 +39,12 @@ class RouteBlindingTestsCommon : LightningTestSuite() {
ByteVector("0121000000000000000000000000000000000000000000000000000000000000000000 04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") to RouteBlindingEncryptedData(
TlvStream(
Padding(ByteVector("000000000000000000000000000000000000000000000000000000000000000000")),
OutgoingNodeId(PublicKey(ByteVector("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c")))
OutgoingNodeId(NodeId(PublicKey(ByteVector("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"))))
)
),
ByteVector("0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007 0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f") to RouteBlindingEncryptedData(
TlvStream(
OutgoingNodeId(PublicKey(ByteVector("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"))),
OutgoingNodeId(NodeId(PublicKey(ByteVector("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")))),
NextBlinding(PublicKey(ByteVector("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")))
)
),
Expand Down
Loading