Skip to content

Commit

Permalink
Requesting BIP353 address from peer (#683)
Browse files Browse the repository at this point in the history
The peer will only answer if there is a channel.

Optionally pass an IETF BCP 47 language tag (en, fr, de, es, ...) to indicate preference for the words that make up the address.
  • Loading branch information
pm47 authored Jul 9, 2024
1 parent 3d2499a commit 5811daa
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 0 deletions.
26 changes: 26 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<String>()
this.launch {
eventsFlow
.filterIsInstance<AddressAssigned>()
.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()
Expand Down Expand Up @@ -1188,6 +1210,10 @@ class Peer(
}
}
}
is DNSAddressResponse -> {
logger.info { "bip353 dns address assigned: ${msg.address}" }
_eventsFlow.emit(AddressAssigned(msg.address))
}
}
}
is WatchReceived -> {
Expand Down
57 changes: 57 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<DNSAddressRequest> {
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<DNSAddressResponse> {
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].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, "[email protected]")
assertEquals(msg, LightningMessage.decode(LightningMessage.encode(msg)))
}
}

0 comments on commit 5811daa

Please sign in to comment.