Skip to content

Commit

Permalink
Expose private key for default offer (#671)
Browse files Browse the repository at this point in the history
This key can be used as a payer key when paying offers to serve as our new identifier (the old node id is not exposed at all with BOLT 12).
  • Loading branch information
thomash-acinq authored Jun 18, 2024
1 parent 95688c8 commit 6903cb8
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 19 deletions.
3 changes: 2 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,9 @@ data class NodeParams(
/**
* We generate a default, deterministic Bolt 12 offer based on the node's seed and its trampoline node.
* This offer will stay valid after restoring the seed on a different device.
* @return the default offer and the private key that will sign invoices for this offer.
*/
fun defaultOffer(trampolineNodeId: PublicKey): OfferTypes.Offer {
fun defaultOffer(trampolineNodeId: PublicKey): Pair<OfferTypes.Offer, PrivateKey> {
// We generate a deterministic blindingSecret based on:
// - a custom tag indicating that this is used in the Bolt 12 context
// - our trampoline node, which is used as an introduction node for the offer's blinded path
Expand Down
30 changes: 19 additions & 11 deletions src/commonMain/kotlin/fr/acinq/lightning/message/OnionMessages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,32 @@ object OnionMessages {
}
}

fun buildRouteToRecipient(
blindingSecret: PrivateKey,
intermediateNodes: List<IntermediateNode>,
recipient: Destination.Recipient
): RouteBlinding. BlindedRouteDetails {
val intermediatePayloads = buildIntermediatePayloads(intermediateNodes, recipient.nodeId)
val tlvs = setOfNotNull(
recipient.padding?.let { RouteBlindingEncryptedDataTlv.Padding(it) },
recipient.pathId?.let { RouteBlindingEncryptedDataTlv.PathId(it) }
)
val lastPayload = RouteBlindingEncryptedData(TlvStream(tlvs, recipient.customTlvs)).write().toByteVector()
return RouteBlinding.create(
blindingSecret,
intermediateNodes.map { it.nodeId.publicKey } + recipient.nodeId.publicKey,
intermediatePayloads + lastPayload
)
}

fun buildRoute(
blindingSecret: PrivateKey,
intermediateNodes: List<IntermediateNode>,
destination: Destination
): RouteBlinding.BlindedRoute {
return when (destination) {
is Destination.Recipient -> {
val intermediatePayloads = buildIntermediatePayloads(intermediateNodes, destination.nodeId)
val tlvs = setOfNotNull(
destination.padding?.let { RouteBlindingEncryptedDataTlv.Padding(it) },
destination.pathId?.let { RouteBlindingEncryptedDataTlv.PathId(it) }
)
val lastPayload = RouteBlindingEncryptedData(TlvStream(tlvs, destination.customTlvs)).write().toByteVector()
RouteBlinding.create(
blindingSecret,
intermediateNodes.map { it.nodeId.publicKey } + destination.nodeId.publicKey,
intermediatePayloads + lastPayload
).route
buildRouteToRecipient(blindingSecret, intermediateNodes, destination).route
}
is Destination.BlindedPath -> when {
intermediateNodes.isEmpty() -> destination.route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
private val localOffers: HashMap<ByteVector32, OfferTypes.Offer> = HashMap()

init {
registerOffer(nodeParams.defaultOffer(walletParams.trampolineNode.id), null)
registerOffer(nodeParams.defaultOffer(walletParams.trampolineNode.id).first, null)
}

fun registerOffer(offer: OfferTypes.Offer, pathId: ByteVector32?) {
Expand Down
8 changes: 4 additions & 4 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -784,18 +784,18 @@ object OfferTypes {
blindingSecret: PrivateKey,
additionalTlvs: Set<OfferTlv> = setOf(),
customTlvs: Set<GenericTlv> = setOf()
): Offer {
): Pair<Offer, PrivateKey> {
if (description == null) require(amount == null) { "an offer description must be provided if the amount isn't null" }
val path = OnionMessages.buildRoute(blindingSecret, listOf(OnionMessages.IntermediateNode(EncodedNodeId.WithPublicKey.Plain(trampolineNodeId))), OnionMessages.Destination.Recipient(EncodedNodeId.WithPublicKey.Wallet(nodeParams.nodeId), null))
val blindedRouteDetails = OnionMessages.buildRouteToRecipient(blindingSecret, listOf(OnionMessages.IntermediateNode(EncodedNodeId.WithPublicKey.Plain(trampolineNodeId))), OnionMessages.Destination.Recipient(EncodedNodeId.WithPublicKey.Wallet(nodeParams.nodeId), null))
val tlvs: Set<OfferTlv> = setOfNotNull(
if (nodeParams.chainHash != Block.LivenetGenesisBlock.hash) OfferChains(listOf(nodeParams.chainHash)) else null,
amount?.let { OfferAmount(it) },
description?.let { OfferDescription(it) },
features.bolt12Features().let { if (it != Features.empty) OfferFeatures(it) else null },
// Note that we don't include an offer_node_id since we're using a blinded path.
OfferPaths(listOf(ContactInfo.BlindedPath(path))),
OfferPaths(listOf(ContactInfo.BlindedPath(blindedRouteDetails.route))),
)
return Offer(TlvStream(tlvs + additionalTlvs, customTlvs))
return Pair(Offer(TlvStream(tlvs + additionalTlvs, customTlvs)), blindedRouteDetails.blindedPrivateKey(nodeParams.nodePrivateKey))
}

fun validate(records: TlvStream<OfferTlv>): Either<InvalidTlvPayload, Offer> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class OfferManagerTestsCommon : LightningTestSuite() {

private fun createOffer(offerManager: OfferManager, amount: MilliSatoshi? = null): OfferTypes.Offer {
val blindingSecret = randomKey()
val offer = OfferTypes.Offer.createBlindedOffer(
val (offer, _) = OfferTypes.Offer.createBlindedOffer(
amount,
"Blockaccino",
offerManager.nodeParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {
fun `generate deterministic blinded offer through trampoline node`() {
val trampolineNode = PublicKey.fromHex("03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")
val nodeParams = TestConstants.Alice.nodeParams.copy(chain = Chain.Mainnet)
val offer = nodeParams.defaultOffer(trampolineNode)
val (offer, key) = nodeParams.defaultOffer(trampolineNode)
assertNull(offer.amount)
assertNull(offer.description)
assertEquals(Features.empty, offer.features) // the offer shouldn't have any feature to guarantee stability
Expand All @@ -518,6 +518,7 @@ class OfferTypesTestsCommon : LightningTestSuite() {
val path = offer.contactInfos.first()
assertIs<BlindedPath>(path)
assertEquals(EncodedNodeId(trampolineNode), path.route.introductionNodeId)
assertEquals(key.publicKey(), path.route.blindedNodeIds.last())
val expectedOffer = Offer.decode("lno1zrxq8pjw7qjlm68mtp7e3yvxee4y5xrgjhhyf2fxhlphpckrvevh50u0qf70a6j2x2akrhazctejaaqr8y4qtzjtjzmfesay6mzr3s789uryuqsr8dpgfgxuk56vh7cl89769zdpdrkqwtypzhu2t8ehp73dqeeq65lsqvlx5pj8mw2kz54p4f6ct66stdfxz0df8nqq7svjjdjn2dv8sz28y7z07yg3vqyfyy8ywevqc8kzp36lhd5cqwlpkg8vdcqsfvz89axkmv5sgdysmwn95tpsct6mdercmz8jh2r82qqscrf6uc3tse5gw5sv5xjdfw8f6c").get()
assertEquals(expectedOffer, offer)
}
Expand Down

0 comments on commit 6903cb8

Please sign in to comment.