From 7bf70c1481c57f390c13a964ce63f23a931306be Mon Sep 17 00:00:00 2001 From: pm47 Date: Mon, 8 Jul 2024 14:38:22 +0200 Subject: [PATCH 1/4] add support for requesting bolt353 dns address --- .../kotlin/fr/acinq/lightning/io/Peer.kt | 19 ++++++++ .../acinq/lightning/wire/LightningMessages.kt | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index e0766d7e0..0f4c29c78 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -12,6 +12,7 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.ChannelCommand.Commitment.Splice.Response import fr.acinq.lightning.channel.states.* import fr.acinq.lightning.crypto.noise.* import fr.acinq.lightning.db.* @@ -117,6 +118,8 @@ data class ChannelClosing(val channelId: ByteVector32) : PeerEvent() */ data class PhoenixAndroidLegacyInfoEvent(val info: PhoenixAndroidLegacyInfo) : PeerEvent() +data class AddressAssigned(val address: String) : PeerEvent() + /** * The peer we establish a connection to. This object contains the TCP socket, a flow of the channels with that peer, and watches * the events on those channels and processes the relevant actions. The dialogue with the peer is done in coroutines. @@ -712,6 +715,18 @@ class Peer( peerConnection?.send(message) } + suspend fun requestAddress(languageSubtag: String): String { + val replyTo = CompletableDeferred() + this.launch { + eventsFlow + .filterIsInstance() + .first() + .let { event -> replyTo.complete(event.address) } + } + peerConnection?.send(DNSAddressRequest(nodeParams.defaultOffer(walletParams.trampolineNode.id).first, languageSubtag)) + return replyTo.await() + } + sealed class SelectChannelResult { /** We have a channel that is available for payments and splicing. */ data class Available(val channel: Normal) : SelectChannelResult() @@ -1188,6 +1203,10 @@ class Peer( } } } + is DNSAddressResponse -> { + logger.info { "dns address assigned: ${msg}" } + _eventsFlow.emit(AddressAssigned(msg.address)) + } } } is WatchReceived -> { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index aba62a09a..94d4e6b7d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -14,6 +14,8 @@ import fr.acinq.lightning.logging.* import fr.acinq.lightning.router.Announcements import fr.acinq.lightning.utils.* import fr.acinq.secp256k1.Hex +import io.ktor.utils.io.charsets.* +import io.ktor.utils.io.core.* import kotlin.math.max import kotlin.math.min @@ -81,6 +83,8 @@ interface LightningMessage { PayToOpenResponse.type -> PayToOpenResponse.read(stream) FCMToken.type -> FCMToken.read(stream) UnsetFCMToken.type -> UnsetFCMToken + DNSAddressRequest.type -> DNSAddressRequest.read(stream) + DNSAddressResponse.type -> DNSAddressResponse.read(stream) PhoenixAndroidLegacyInfo.type -> PhoenixAndroidLegacyInfo.read(stream) PleaseOpenChannel.type -> PleaseOpenChannel.read(stream) Stfu.type -> Stfu.read(stream) @@ -1747,6 +1751,50 @@ data class PhoenixAndroidLegacyInfo( } } +data class DNSAddressRequest(val offer: OfferTypes.Offer, val languageSubtype: String) : LightningMessage { + + override val type: Long get() = DNSAddressRequest.type + + override fun write(out: Output) { + val serializedOffer = offer.encode() + LightningCodecs.writeU16(serializedOffer.length, out) + LightningCodecs.writeBytes(serializedOffer.toByteArray(charset = Charsets.UTF_8), out) + LightningCodecs.writeU16(languageSubtype.length, out) + LightningCodecs.writeBytes(languageSubtype.toByteArray(charset = Charsets.UTF_8), out) + } + + companion object : LightningMessageReader { + const val type: Long = 35025 + + override fun read(input: Input): DNSAddressRequest { + return DNSAddressRequest( + offer = OfferTypes.Offer.decode(LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString()).get(), + languageSubtype = LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString() + ) + } + } +} + +data class DNSAddressResponse(val address: String) : LightningMessage { + + override val type: Long get() = DNSAddressResponse.type + + override fun write(out: Output) { + LightningCodecs.writeU16(address.length, out) + LightningCodecs.writeBytes(address.toByteArray(charset = Charsets.UTF_8), out) + } + + companion object : LightningMessageReader { + const val type: Long = 35027 + + override fun read(input: Input): DNSAddressResponse { + return DNSAddressResponse( + address = LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString() + ) + } + } +} + /** * This message is used to request a channel open from a remote node, with local contributions to the funding transaction. * If the remote node won't open a channel, it will respond with [PleaseOpenChannelRejected]. From ad3b2efae4c8783f39df86602600376039171a5d Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 9 Jul 2024 15:45:57 +0200 Subject: [PATCH 2/4] review comments from @t-bast --- .../kotlin/fr/acinq/lightning/io/Peer.kt | 9 +++++-- .../acinq/lightning/wire/LightningMessages.kt | 25 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 0f4c29c78..293ea232a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -715,6 +715,11 @@ class Peer( peerConnection?.send(message) } + /** + * Request a BIP-353's compliant DNS address from our peer. + * + * @param languageSubtag IETF BCP 47 language tag (en, fr, de, es, ...) to indicate preference for the words that make up the address + */ suspend fun requestAddress(languageSubtag: String): String { val replyTo = CompletableDeferred() this.launch { @@ -723,7 +728,7 @@ class Peer( .first() .let { event -> replyTo.complete(event.address) } } - peerConnection?.send(DNSAddressRequest(nodeParams.defaultOffer(walletParams.trampolineNode.id).first, languageSubtag)) + peerConnection?.send(DNSAddressRequest(nodeParams.chainHash, nodeParams.defaultOffer(walletParams.trampolineNode.id).first, languageSubtag)) return replyTo.await() } @@ -1204,7 +1209,7 @@ class Peer( } } is DNSAddressResponse -> { - logger.info { "dns address assigned: ${msg}" } + logger.info { "bip353 dns address assigned: ${msg.address}" } _eventsFlow.emit(AddressAssigned(msg.address)) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 94d4e6b7d..235f465d7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -1751,16 +1751,22 @@ data class PhoenixAndroidLegacyInfo( } } -data class DNSAddressRequest(val offer: OfferTypes.Offer, val languageSubtype: String) : LightningMessage { +/** + * A message to request a BIP-353's compliant DNS address from our peer. The peer may not respond, e.g. if there are no channels. + * + * @param languageSubtag IETF BCP 47 language tag (en, fr, de, es, ...) to indicate preference for the words that make up the address + */ +data class DNSAddressRequest(override val chainHash: BlockHash, val offer: OfferTypes.Offer, val languageSubtag: String) : LightningMessage, HasChainHash { override val type: Long get() = DNSAddressRequest.type override fun write(out: Output) { - val serializedOffer = offer.encode() - LightningCodecs.writeU16(serializedOffer.length, out) - LightningCodecs.writeBytes(serializedOffer.toByteArray(charset = Charsets.UTF_8), out) - LightningCodecs.writeU16(languageSubtype.length, out) - LightningCodecs.writeBytes(languageSubtype.toByteArray(charset = Charsets.UTF_8), out) + LightningCodecs.writeBytes(chainHash.value, out) + val serializedOffer = OfferTypes.Offer.tlvSerializer.write(offer.records) + LightningCodecs.writeU16(serializedOffer.size, out) + LightningCodecs.writeBytes(serializedOffer, out) + LightningCodecs.writeU16(languageSubtag.length, out) + LightningCodecs.writeBytes(languageSubtag.toByteArray(charset = Charsets.UTF_8), out) } companion object : LightningMessageReader { @@ -1768,18 +1774,20 @@ data class DNSAddressRequest(val offer: OfferTypes.Offer, val languageSubtype: S override fun read(input: Input): DNSAddressRequest { return DNSAddressRequest( + chainHash = BlockHash(LightningCodecs.bytes(input, 32)), offer = OfferTypes.Offer.decode(LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString()).get(), - languageSubtype = LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString() + languageSubtag = LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString() ) } } } -data class DNSAddressResponse(val address: String) : LightningMessage { +data class DNSAddressResponse(override val chainHash: BlockHash, val address: String) : LightningMessage, HasChainHash { override val type: Long get() = DNSAddressResponse.type override fun write(out: Output) { + LightningCodecs.writeBytes(chainHash.value, out) LightningCodecs.writeU16(address.length, out) LightningCodecs.writeBytes(address.toByteArray(charset = Charsets.UTF_8), out) } @@ -1789,6 +1797,7 @@ data class DNSAddressResponse(val address: String) : LightningMessage { override fun read(input: Input): DNSAddressResponse { return DNSAddressResponse( + chainHash = BlockHash(LightningCodecs.bytes(input, 32)), address = LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString() ) } From 146ffd4ba318bf7e688d8240153beb99c414ef05 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 9 Jul 2024 16:21:13 +0200 Subject: [PATCH 3/4] document hang behavior --- src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 293ea232a..ce09b5001 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -718,6 +718,8 @@ class Peer( /** * Request a BIP-353's compliant DNS address from our peer. * + * This will only return if there are existing channels with the peer, otherwise it will hang. This should be handled by the caller. + * * @param languageSubtag IETF BCP 47 language tag (en, fr, de, es, ...) to indicate preference for the words that make up the address */ suspend fun requestAddress(languageSubtag: String): String { From d1daf8de522c9eea0f631b5885140d682f78bdfd Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 9 Jul 2024 16:46:26 +0200 Subject: [PATCH 4/4] add unit tests --- .../fr/acinq/lightning/wire/LightningMessages.kt | 2 +- .../lightning/wire/LightningCodecsTestsCommon.kt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 235f465d7..f314e2cec 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -1775,7 +1775,7 @@ data class DNSAddressRequest(override val chainHash: BlockHash, val offer: Offer override fun read(input: Input): DNSAddressRequest { return DNSAddressRequest( chainHash = BlockHash(LightningCodecs.bytes(input, 32)), - offer = OfferTypes.Offer.decode(LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString()).get(), + offer = OfferTypes.Offer(OfferTypes.Offer.tlvSerializer.read(LightningCodecs.bytes(input, LightningCodecs.u16(input)))), languageSubtag = LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString() ) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index eedb45424..1c9706096 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -17,6 +17,7 @@ import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.wire.OfferTypes.Offer import fr.acinq.secp256k1.Hex import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray @@ -874,4 +875,19 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val onionMessage = OnionMessages.buildMessage(randomKey(), randomKey(), listOf(), OnionMessages.Destination.Recipient(EncodedNodeId(randomKey().publicKey()), null), TlvStream.empty()).right!! assertEquals(onionMessage, OnionMessage.read(onionMessage.write())) } + + @Test + fun `encode and decode dns address request`() { + val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968jys3v9kxjcm9gp3xjemndphhqtnrdak3gqqkyypsmuhrtwfzm85mht4a3vcp0yrlgua3u3m5uqpc6kf7nqjz6v70qwg" + val offer = Offer.decode(encoded).get() + + val msg = DNSAddressRequest(Chain.Testnet.chainHash, offer, "en") + assertEquals(msg, LightningMessage.decode(LightningMessage.encode(msg))) + } + + @Test + fun `encode and decode dns address response`() { + val msg = DNSAddressResponse(Chain.Testnet.chainHash, "foo@bar.baz") + assertEquals(msg, LightningMessage.decode(LightningMessage.encode(msg))) + } } \ No newline at end of file