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

Requesting BIP353 address from peer #683

Merged
merged 4 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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 {
pm47 marked this conversation as resolved.
Show resolved Hide resolved
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
t-bast marked this conversation as resolved.
Show resolved Hide resolved

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)))
}
}
Loading