Skip to content

Commit

Permalink
Rename OfferIssuerId (#720)
Browse files Browse the repository at this point in the history
The `OfferIssuerId` was previously named `OfferNodeId`, but its usage
changed when it was renamed: it doesn't necessarily match a lightning
`node_id` and can instead be a completely unrelated public key.

It can now be included in combination with blinded paths: when that
happens, the blinded paths must be used in priority to reach the node
as the `issuer_id` may not match a network `node_id` at all (or may
even match an unrelated node).
  • Loading branch information
t-bast authored Oct 17, 2024
1 parent ff19651 commit f86fffe
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ object JsonSerializers {
val description: String?,
val metadata: ByteVector?,
val expirySeconds: Long?,
val nodeId: PublicKey?,
val issuerId: PublicKey?,
val path: List<RouteBlinding.BlindedRoute>?,
val features: Features?,
val unknownTlvs: List<GenericTlv>?
Expand All @@ -690,7 +690,7 @@ object JsonSerializers {
description = o.description,
metadata = o.metadata,
expirySeconds = o.expirySeconds,
nodeId = o.nodeId,
issuerId = o.issuerId,
path = o.paths?.map { it.route }?.run { ifEmpty { null } },
features = o.features.let { if (it == Features.empty) null else it },
unknownTlvs = o.records.unknown.toList().run { ifEmpty { null } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ data class Bolt12Invoice(val records: TlvStream<InvoiceTlv>) : PaymentRequest()

// It is assumed that the request is valid for this offer.
fun validateFor(request: InvoiceRequest): Either<String, Unit> {
val offerNodeIds = invoiceRequest.offer.nodeId?.let { listOf(it) } ?: invoiceRequest.offer.paths!!.map { it.route.blindedNodeIds.last() }
val offerNodeIds = invoiceRequest.offer.issuerId?.let { listOf(it) } ?: invoiceRequest.offer.paths!!.map { it.route.blindedNodeIds.last() }
return if (invoiceRequest.unsigned() != request.unsigned()) {
Either.Left("Invoice does not match request")
} else if (!offerNodeIds.contains(nodeId)) {
Expand Down
30 changes: 15 additions & 15 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -262,21 +262,21 @@ object OfferTypes {
}

/**
* Public key of the offer creator.
* Public key of the offer issuer.
* If `OfferPaths` is present, they must be used to retrieve an invoice even if this public key corresponds to a node id in the public network.
* If `OfferPaths` is not present, this public key must correspond to a node id in the public network that needs to be contacted to retrieve an invoice.
*/
data class OfferNodeId(val publicKey: PublicKey) : OfferTlv() {
override val tag: Long get() = OfferNodeId.tag
data class OfferIssuerId(val publicKey: PublicKey) : OfferTlv() {
override val tag: Long get() = OfferIssuerId.tag

override fun write(out: Output) {
LightningCodecs.writeBytes(publicKey.value, out)
}

companion object : TlvValueReader<OfferNodeId> {
companion object : TlvValueReader<OfferIssuerId> {
const val tag: Long = 22
override fun read(input: Input): OfferNodeId {
return OfferNodeId(PublicKey(LightningCodecs.bytes(input, input.availableBytes)))
override fun read(input: Input): OfferIssuerId {
return OfferIssuerId(PublicKey(LightningCodecs.bytes(input, input.availableBytes)))
}
}
}
Expand Down Expand Up @@ -732,10 +732,10 @@ object OfferTypes {
val paths: List<ContactInfo.BlindedPath>? = records.get<OfferPaths>()?.paths
val issuer: String? = records.get<OfferIssuer>()?.issuer
val quantityMax: Long? = records.get<OfferQuantityMax>()?.max?.let { if (it == 0L) Long.MAX_VALUE else it }
val nodeId: PublicKey? = records.get<OfferNodeId>()?.publicKey
// A valid offer must contain a blinded path or a nodeId.
val contactInfos: List<ContactInfo> = paths ?: listOf(ContactInfo.RecipientNodeId(nodeId!!))
val contactNodeIds: List<PublicKey> = contactInfos.map { it.nodeId }
val issuerId: PublicKey? = records.get<OfferIssuerId>()?.publicKey
// A valid offer must contain a blinded path or an issuerId (and may contain both).
// When both are provided, the blinded paths should be tried first.
val contactInfos: List<ContactInfo> = paths ?: listOf(ContactInfo.RecipientNodeId(issuerId!!))

fun encode(): String {
val data = tlvSerializer.write(records)
Expand Down Expand Up @@ -773,7 +773,7 @@ object OfferTypes {
amount?.let { OfferAmount(it) },
description?.let { OfferDescription(it) },
features.bolt12Features().let { if (it != Features.empty) OfferFeatures(it) else null },
OfferNodeId(nodeId)
OfferIssuerId(nodeId)
)
return Offer(TlvStream(tlvs + additionalTlvs, customTlvs))
}
Expand Down Expand Up @@ -813,7 +813,7 @@ object OfferTypes {

fun validate(records: TlvStream<OfferTlv>): Either<InvalidTlvPayload, Offer> {
if (records.get<OfferDescription>() == null && records.get<OfferAmount>() != null) return Left(MissingRequiredTlv(10))
if (records.get<OfferNodeId>() == null && records.get<OfferPaths>() == null) return Left(MissingRequiredTlv(22))
if (records.get<OfferIssuerId>() == null && records.get<OfferPaths>() == null) return Left(MissingRequiredTlv(22))
if (records.unknown.any { !isOfferTlv(it) }) return Left(ForbiddenTlv(records.unknown.find { !isOfferTlv(it) }!!.tag))
return Right(Offer(records))
}
Expand All @@ -830,7 +830,7 @@ object OfferTypes {
OfferPaths.tag to OfferPaths as TlvValueReader<OfferTlv>,
OfferIssuer.tag to OfferIssuer as TlvValueReader<OfferTlv>,
OfferQuantityMax.tag to OfferQuantityMax as TlvValueReader<OfferTlv>,
OfferNodeId.tag to OfferNodeId as TlvValueReader<OfferTlv>,
OfferIssuerId.tag to OfferIssuerId as TlvValueReader<OfferTlv>,
)
)

Expand Down Expand Up @@ -958,7 +958,7 @@ object OfferTypes {
OfferPaths.tag to OfferPaths as TlvValueReader<InvoiceRequestTlv>,
OfferIssuer.tag to OfferIssuer as TlvValueReader<InvoiceRequestTlv>,
OfferQuantityMax.tag to OfferQuantityMax as TlvValueReader<InvoiceRequestTlv>,
OfferNodeId.tag to OfferNodeId as TlvValueReader<InvoiceRequestTlv>,
OfferIssuerId.tag to OfferIssuerId as TlvValueReader<InvoiceRequestTlv>,
// Invoice request part
InvoiceRequestChain.tag to InvoiceRequestChain as TlvValueReader<InvoiceRequestTlv>,
InvoiceRequestAmount.tag to InvoiceRequestAmount as TlvValueReader<InvoiceRequestTlv>,
Expand Down Expand Up @@ -998,7 +998,7 @@ object OfferTypes {
OfferPaths.tag to OfferPaths as TlvValueReader<InvoiceTlv>,
OfferIssuer.tag to OfferIssuer as TlvValueReader<InvoiceTlv>,
OfferQuantityMax.tag to OfferQuantityMax as TlvValueReader<InvoiceTlv>,
OfferNodeId.tag to OfferNodeId as TlvValueReader<InvoiceTlv>,
OfferIssuerId.tag to OfferIssuerId as TlvValueReader<InvoiceTlv>,
InvoiceRequestChain.tag to InvoiceRequestChain as TlvValueReader<InvoiceTlv>,
InvoiceRequestAmount.tag to InvoiceRequestAmount as TlvValueReader<InvoiceTlv>,
InvoiceRequestFeatures.tag to InvoiceRequestFeatures as TlvValueReader<InvoiceTlv>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import fr.acinq.lightning.wire.OfferTypes.OfferChains
import fr.acinq.lightning.wire.OfferTypes.OfferDescription
import fr.acinq.lightning.wire.OfferTypes.OfferFeatures
import fr.acinq.lightning.wire.OfferTypes.OfferIssuer
import fr.acinq.lightning.wire.OfferTypes.OfferNodeId
import fr.acinq.lightning.wire.OfferTypes.OfferIssuerId
import fr.acinq.lightning.wire.OfferTypes.OfferQuantityMax
import fr.acinq.lightning.wire.OfferTypes.PaymentInfo
import fr.acinq.lightning.wire.OfferTypes.Signature
Expand Down Expand Up @@ -173,7 +173,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() {
val otherNodeKey = randomKey()
val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map {
when (it) {
is OfferNodeId -> OfferNodeId(otherNodeKey.publicKey())
is OfferIssuerId -> OfferIssuerId(otherNodeKey.publicKey())
else -> it
}
}.toSet())), nodeKey)
Expand Down Expand Up @@ -236,7 +236,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() {
val tlvs = setOf(
InvoiceRequestMetadata(ByteVector.fromHex("010203040506")),
OfferDescription("offer description"),
OfferNodeId(nodeKey.publicKey()),
OfferIssuerId(nodeKey.publicKey()),
InvoiceRequestAmount(15000.msat),
InvoiceRequestPayerId(payerKey.publicKey()),
InvoiceRequestPayerNote("I am Batman"),
Expand Down Expand Up @@ -306,7 +306,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() {
val nodeKey = randomKey()
val tlvs = setOf(
InvoiceRequestMetadata(ByteVector.fromHex("012345")),
OfferNodeId(nodeKey.publicKey()),
OfferIssuerId(nodeKey.publicKey()),
InvoiceRequestPayerId(randomKey().publicKey()),
InvoicePaths(listOf(createPaymentBlindedRoute(randomKey().publicKey()).route)),
InvoiceBlindedPay(listOf(PaymentInfo(0.msat, 0, CltvExpiryDelta(0), 0.msat, 765432.msat, Features.empty))),
Expand Down Expand Up @@ -360,7 +360,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() {
OfferDescription(description),
OfferFeatures(Features.empty),
OfferIssuer(issuer),
OfferNodeId(nodeKey.publicKey()),
OfferIssuerId(nodeKey.publicKey()),
InvoiceRequestChain(chain),
InvoiceRequestAmount(amount),
InvoiceRequestQuantity(quantity),
Expand Down Expand Up @@ -489,7 +489,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() {
OfferDescription("offer with quantity"),
OfferIssuer("[email protected]"),
OfferQuantityMax(1000),
OfferNodeId(nodeKey.publicKey())
OfferIssuerId(nodeKey.publicKey())
)
)
val encodedOffer = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2zdhkven9wgs8w6t5dqs8zatpde6xjarezggkzmrfvdj5qcnfvaeksmms9e3k7mg5qgp7s93pqvn6l4vemgezdarq3wt2kpp0u4vt74vzz8futen7ej97n93jypp57"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import fr.acinq.lightning.wire.OfferTypes.OfferAmount
import fr.acinq.lightning.wire.OfferTypes.OfferChains
import fr.acinq.lightning.wire.OfferTypes.OfferDescription
import fr.acinq.lightning.wire.OfferTypes.OfferIssuer
import fr.acinq.lightning.wire.OfferTypes.OfferNodeId
import fr.acinq.lightning.wire.OfferTypes.OfferIssuerId
import fr.acinq.lightning.wire.OfferTypes.OfferQuantityMax
import fr.acinq.lightning.wire.OfferTypes.Signature
import fr.acinq.lightning.wire.OfferTypes.readPath
Expand All @@ -55,13 +55,13 @@ class OfferTypesTestsCommon : LightningTestSuite() {

@Test
fun `minimal offer`() {
val tlvs = setOf(OfferNodeId(nodeId))
val tlvs = setOf(OfferIssuerId(nodeId))
val offer = Offer(TlvStream(tlvs))
val encoded = "lno1zcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe"
assertEquals(offer, Offer.decode(encoded).get())
assertNull(offer.amount)
assertNull(offer.description)
assertEquals(nodeId, offer.nodeId)
assertEquals(nodeId, offer.issuerId)
// We can't create an empty offer.
assertTrue(Offer.validate(TlvStream.empty()).isLeft)
}
Expand All @@ -75,14 +75,14 @@ class OfferTypesTestsCommon : LightningTestSuite() {
OfferDescription("offer with quantity"),
OfferIssuer("[email protected]"),
OfferQuantityMax(0),
OfferNodeId(nodeId)
OfferIssuerId(nodeId)
)
)
val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968jys3v9kxjcm9gp3xjemndphhqtnrdak3gqqkyypsmuhrtwfzm85mht4a3vcp0yrlgua3u3m5uqpc6kf7nqjz6v70qwg"
assertEquals(offer, Offer.decode(encoded).get())
assertEquals(50.msat, offer.amount)
assertEquals("offer with quantity", offer.description)
assertEquals(nodeId, offer.nodeId)
assertEquals(nodeId, offer.issuerId)
assertEquals("[email protected]", offer.issuer)
assertEquals(Long.MAX_VALUE, offer.quantityMax)
}
Expand Down Expand Up @@ -149,7 +149,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {

@Test
fun `check that invoice request matches offer - without chain`() {
val offer = Offer(TlvStream(OfferAmount(100.msat), OfferDescription("offer without chains"), OfferNodeId(randomKey().publicKey())))
val offer = Offer(TlvStream(OfferAmount(100.msat), OfferDescription("offer without chains"), OfferIssuerId(randomKey().publicKey())))
val payerKey = randomKey()
val tlvs: Set<InvoiceRequestTlv> = offer.records.records + setOf(
InvoiceRequestMetadata(ByteVector.fromHex("012345")),
Expand All @@ -171,7 +171,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {
fun `check that invoice request matches offer - with chains`() {
val chain1 = BlockHash(randomBytes32())
val chain2 = BlockHash(randomBytes32())
val offer = Offer(TlvStream(OfferChains(listOf(chain1, chain2)), OfferAmount(100.msat), OfferDescription("offer with chains"), OfferNodeId(randomKey().publicKey())))
val offer = Offer(TlvStream(OfferChains(listOf(chain1, chain2)), OfferAmount(100.msat), OfferDescription("offer with chains"), OfferIssuerId(randomKey().publicKey())))
val payerKey = randomKey()
val request1 = InvoiceRequest(offer, 100.msat, 1, Features.empty, payerKey, null, chain1)
assertTrue(request1.isValid())
Expand All @@ -196,7 +196,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {
TlvStream(
OfferAmount(500.msat),
OfferDescription("offer for multiple items"),
OfferNodeId(randomKey().publicKey()),
OfferIssuerId(randomKey().publicKey()),
OfferQuantityMax(10),
)
)
Expand All @@ -216,7 +216,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {
val payerKey = PrivateKey.fromHex("527d410ec920b626ece685e8af9abc976a48dbf2fe698c1b35d90a1c5fa2fbca")
val tlvsWithoutSignature = setOf(
InvoiceRequestMetadata(ByteVector.fromHex("abcdef")),
OfferNodeId(nodeId),
OfferIssuerId(nodeId),
InvoiceRequestPayerId(payerKey.publicKey()),
)
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvsWithoutSignature)), payerKey)
Expand All @@ -226,7 +226,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {
assertEquals(invoiceRequest, InvoiceRequest.decode(encoded).get())
assertNull(invoiceRequest.offer.amount)
assertNull(invoiceRequest.offer.description)
assertEquals(nodeId, invoiceRequest.offer.nodeId)
assertEquals(nodeId, invoiceRequest.offer.issuerId)
assertEquals(ByteVector.fromHex("abcdef"), invoiceRequest.metadata)
assertEquals(payerKey.publicKey(), invoiceRequest.payerId)
// Removing any TLV from the minimal invoice request makes it invalid.
Expand Down Expand Up @@ -514,7 +514,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {
assertNull(offer.description)
assertEquals(Features.empty, offer.features) // the offer shouldn't have any feature to guarantee stability
assertNull(offer.expirySeconds)
assertNull(offer.nodeId) // the offer should not leak our node_id
assertNull(offer.issuerId) // the offer should not leak our node_id
assertEquals(1, offer.contactInfos.size)
val path = offer.contactInfos.first()
assertIs<BlindedPath>(path)
Expand Down

0 comments on commit f86fffe

Please sign in to comment.