diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index e0766d7e0..ce09b5001 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,25 @@ class Peer( peerConnection?.send(message) } + /** + * 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 { + val replyTo = CompletableDeferred() + this.launch { + eventsFlow + .filterIsInstance() + .first() + .let { event -> replyTo.complete(event.address) } + } + peerConnection?.send(DNSAddressRequest(nodeParams.chainHash, 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 +1210,10 @@ class Peer( } } } + is DNSAddressResponse -> { + logger.info { "bip353 dns address assigned: ${msg.address}" } + _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..f314e2cec 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,59 @@ data class PhoenixAndroidLegacyInfo( } } +/** + * 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) { + 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 { + const val type: Long = 35025 + + override fun read(input: Input): DNSAddressRequest { + return DNSAddressRequest( + chainHash = BlockHash(LightningCodecs.bytes(input, 32)), + offer = OfferTypes.Offer(OfferTypes.Offer.tlvSerializer.read(LightningCodecs.bytes(input, LightningCodecs.u16(input)))), + languageSubtag = LightningCodecs.bytes(input, LightningCodecs.u16(input)).decodeToString() + ) + } + } +} + +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) + } + + companion object : LightningMessageReader { + const val type: Long = 35027 + + override fun read(input: Input): DNSAddressResponse { + return DNSAddressResponse( + chainHash = BlockHash(LightningCodecs.bytes(input, 32)), + 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]. 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