bLIP: 42
Title: Bolt 12 Contacts
Status: Active
Author: Bastien Teinturier <bastien@acinq.fr>
Created: 2024-07-19
License: CC0
Bolt 12 introduces offers, which are static lightning "addresses". An offer can be stored and reused to pay the same node many times. It then becomes natural to associate Bolt 12 offers to your friends and contacts.
When sending payments to contacts, you may want them to know that the payment came from you. We propose a scheme to optionally include contact information in outgoing payments to allow the recipient to:
- detect that the payment is coming from one of their known contacts
- otherwise, be able to add the payer to their contacts list
- send funds back to the payer without additional interaction
This feature provides a better UX for lightning wallets, by making payments between contacts look very similar to fiat payment applications.
This bLIP is licensed under the CC0 license.
This feature provides a better UX for lightning wallets, by making payments between contacts look very similar to fiat payment applications.
The invreq_contact_secret field is an identifier for a contact pair:
- type: 2000001729 (
invreq_contact_secret) - data:
- [
32*byte:contact_secret]
- [
The invreq_payer_offer field lets payers reveal a Bolt 12 offer that can
be used by contacts to pay them back:
- type: 2000001731 (
invreq_payer_offer) - data:
- [
...*byte:payer_offer]
- [
The invreq_payer_bip_353_name field lets payers reveal their BIP 353 name
to allow contacts to pay them back:
- type: 2000001733 (
invreq_payer_bip_353_name) - data:
- [
u8:name_len] - [
name_len*byte:name] - [
u8:domain_len] - [
domain_len*byte:domain]
- [
The invreq_payer_bip_353_signature field lets payers provide a signature of
the invoice_request using one of the signing keys of the offer associated
with their BIP 353 name, thus proving ownership of this BIP 353 name.
- type: 2000001735 (
invreq_payer_bip_353_signature) - data:
- [
point:offer_signing_key] - [
bip340sig:signature]
- [
When creating a new payment contact:
- MUST receive user authorization to reveal payment information to the contact.
- If the node has previously received a payment from this contact including the
invreq_contact_secretfield:- MUST associate the received
invreq_contact_secret,invreq_payer_offerandinvreq_payer_bip_353_namewith this contact. - If
invreq_payer_bip_353_namewas included:- SHOULD use it to fetch an
invreq_payer_offerif none was included. - MUST verify that the received offer matches the
offer_signing_keyprovided ininvreq_payer_bip_353_signature. - SHOULD use it to refresh the
invreq_payer_offerif it expires. - MAY use it to refresh the
invreq_payer_offerperiodically.
- SHOULD use it to fetch an
- MUST associate the received
- Otherwise:
- MUST generate a unique
invreq_contact_secretfor that contact, using deterministic derivation. - MUST associate this
invreq_contact_secretwith this contact. - MUST include this
invreq_contact_secretwhenever paying this contact.
- MUST generate a unique
The writer of invoice_request:
- If they want the recipient to be able to identify who paid them:
- If the recipient is not a payment contact:
- MUST seek user authorization to create a payment contact matching that recipient.
- MUST include the
invreq_contact_secretassociated with this contact. - MUST include either
invreq_payer_offerorinvreq_payer_bip_353_name. - If it includes
invreq_payer_bip_353_name:- MUST set
nameto the post-₿, pre-@ part of the BIP 353 HRN. - MUST set
domainto the post-@ part of the BIP 353 HRN. - MUST include
invreq_payer_bip_353_signatureusing the private key of itsoffer_issuer_idor, if its offer doesn't includeoffer_issuer_id, the private key of theblinded_node_idof one of itsoffer_paths. The signed data should be computed as detailed in Bolt 12, with "lightning" || "invoice_request" || "invreq_payer_bip_353_signature" as signature tag.
- MUST set
- If it includes
invreq_payer_offer:- MUST encode
payer_offeras a TLV stream of its individual records. - If the encoded offer is more than 300 bytes long:
- SHOULD NOT include
invreq_payer_offer. - SHOULD include
invreq_payer_bip_353_nameinstead.
- SHOULD NOT include
- MUST encode
- If the recipient is not a payment contact:
- Otherwise:
- MUST NOT include
invreq_contact_secret,invreq_payer_offerorinvreq_payer_bip_353_name.
- MUST NOT include
The reader of invoice_request:
- If
invreq_payer_bip_353_nameis provided:- MUST ignore the
invoice_requestifinvreq_payer_bip_353_signatureis missing or invalid.
- MUST ignore the
- MUST send back an
invoiceincluding theinvoice_requestcontact fields provided by the sender, as specified in Bolt 12.
When an invoice has been paid:
- If
invreq_contact_secretwas included in theinvoice_request:- If it matches an existing payment contact:
- SHOULD display the
invreq_payer_note, if one is provided. - MUST ignore
invreq_payer_offerandinvreq_payer_bip_353_name.
- SHOULD display the
- Otherwise:
- MAY seek authorization from the user to add a new contact or update an existing contact based on this received payment (if the user verified that the payment came from this contact).
- If it matches an existing payment contact:
The contact_secret field is used for mutual identification: it is set by the
node sending the first payment and must be reused by the recipient when sending
payments in the other direction. Its usage and edge cases are detailed in the
Contact Secrets section below.
Nodes generally don't store every invoice_request they receive, because that
would expose them to DoS. They instead include the fields they would like to
store in the path_id field of the blinded path(s) of the invoice they send
back. Since this path_id will then be included in payment onions, which are
limited to 1300 bytes, nodes must ensure that the resulting path_id isn't too
large, which would constrain the payment paths that can be used by the payer.
We thus recommend only including offers that are smaller than 300 bytes in
invreq_payer_offer, or a small BIP 353 HRN.
When a BIP 353 HRN is included, the node receiving invoice_request does not
know whether it really belongs to the sender. This is why the sender must also
include a signature that proves that they control one of the private keys of
the offer stored in the BIP 353 record.
When payments are coming from known contacts, there is less risk that the
payer_note that is optionally included contains spam. It is thus recommended
to display it, while we generally don't recommend displaying payer_notes
coming from unknown payers.
When receiving payments from existing contacts, the offer and BIP 353 HRN must
be ignored: this ensures that if the contact_secret was leaked, a malicious
node impersonating our contact cannot redirect our future payments to their
own offers.
The main mechanism of this proposal is the exchange of contact_secrets.
This section details various scenarios that may occur and how to correctly
deal with each of them, along with a recommended UX.
When Alice wants to pay Bob, her wallet should offer an option to add him to
her contacts list (using a checkbox or a dedicated button). If she chooses
that option, she generates a contact_secret (see details about generation
in the deterministic derivation section). For
all future payments made to Bob where she wants to reveal that she's the
payer, Alice will include this contact_secret. Wallets should always offer
the option to pay a contact privately, in which case the contact_secret
and payer information will not be included in the invoice_request.
Once Bob has received a payment that includes a contact_secret, his wallet
should display an option to add the payer to its own contacts list (e.g. via
a dedicated button the received payment page). If he knows that this payment
came from Alice (because Alice verifiably told him that it indeed came from
her), he's able to add Alice to his contacts and pay her back using the
payer_offer or payer_bip_353_name she provided. For all future payments
made to Alice where Bob wants to reveal that he's the payer, Bob will include
the contact_secret generated by Alice. Note that in this case, Bob doesn't
generate a different contact_secret, because he already has one available
that was created by Alice, which he knows Alice will be able to use to identify
payments.
However, if Bob adds Alice to his contacts list without using the payment he
received from her, or if he adds her to his contacts list on another wallet
than the one used to receive Alice's payment, Bob will generate a different
contact_secret. For all payments made to Alice where he wants to reveal that
he's the payer, he will use that new contact_secret. When Alice receives
those payments, she won't be able to automatically identify that it's
coming from Bob based on the contact_secret alone, because it is different
from the one she generated. But if Alice knows that a specific payment came
from Bob (because he verifiably told her so), her wallet should allow her to
attribute this payment to an existing contact (e.g. by clicking an "add to
contacts" button on the received payment and then choosing an "add to existing
contact" option). Her wallet will then add that additional contact_secret to
the list of secrets Bob may use when paying her. This action automatically
reconciles past and future payments made from Bob.
A contact entry thus contains the following information:
primary_contact_secret: the firstcontact_secretused, which must be used for all outgoing payments to this contact and may either have been created by us (if we made the first payment) or by our contact (if we added them to our contacts list based on a payment we received).additional_remote_contact_secrets: a list of secondarycontact_secrets that our contact may use when paying us, obtained by manually associating payments with our existing contact.
Contact secrets shouldn't be shared publicly, as that would let other people
make payments that appear to be coming from you. This doesn't allow stealing
funds though: even if the impersonator includes their own offer in a payment
they make on your behalf in the invreq_payer_offer field, the receiving node
will ignore it if they have already stored your contact information. If they
haven't, they have no reason to create a new contact based on this payment.
When creating a new contact, we use the following deterministic derivation
for the contact_secret field:
- For a given Bolt 12 offer, we define its
offer_node_idas:- If the offer contains
offer_issuer_id:offer_node_id = offer_issuer_id.
- Otherwise, the offer must contain
offer_paths:offer_node_idis set to the lastblinded_node_idof the firstpath.
- If the offer contains
- The private key for the
offer_node_idis calledoffer_priv_key. - When paying
remote_offerfor which we include ourlocal_offerin theinvreq_payer_offerfield:- We compute the ECDH of the two
offer_node_ids:shared_key = local_offer.offer_priv_key * remote_offer.offer_node_id.
- We use a tagged hash to derive the
contact_secret:contact_secret = SHA256("blip42_contact_secret" || shared_key).
- We compute the ECDH of the two
Using this deterministic derivation has multiple benefits. First of all, it
guarantees that both nodes independently derive the same contact_secret when
using the same set of offers, which removes the need to reconcile secrets when
nodes concurrently add each other to their contacts list.
It is particularly useful for wallets that use a single offer that is created
deterministically from the user's seed: this ensures that the contact_secret
can also be restored from seed.
The following test vectors use the deterministic derivation from the previous section.
[
{
"comment": "derive deterministic contact_secret when both offers use blinded paths only",
"alice_offer": "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcsesp0grlulxv3jygx83h7tghy3233sqd6xlcccvpar2l8jshxrtwvtcsrejlwh4vyz70s46r62vtakl4sxztqj6gxjged0wx0ly8qtrygufcsyq5agaes6v605af5rr9ydnj9srneudvrmc73n7evp72tzpqcnd28puqr8a3wmcff9wfjwgk32650vl747m2ev4zsjagzucntctlmcpc6vhmdnxlywneg5caqz0ansr45z2faxq7unegzsnyuduzys7kzyugpwcmhdqqj0h70zy92p75pseunclwsrwhaelvsqy9zsejcytxulndppmykcznn7y5h",
"alice_offer_priv_key": "4ed1a01dae275f7b7ba503dbae23dddd774a8d5f64788ef7a768ed647dd0e1eb",
"alice_offer_node_id": "0284c9c6f04487ac22710176377680127dfcf110aa0fa8186793c7dd01bafdcfd9",
"bob_offer": "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcsesp0grlulxv3jygx83h7tghy3233sqd6xlcccvpar2l8jshxrtwvtcsz4n88s74qhussxsu0vs3c4unck4yelk67zdc29ree3sztvjn7pc9qyqlcpj54jnj67aa9rd2n5dhjlxyfmv3vgqymrks2nf7gnf5u200mn5qrxfrxh9d0ug43j5egklhwgyrfv3n84gyjd2aajhwqxa0cc7zn37sncrwptz4uhlp523l83xpjx9dw72spzecrtex3ku3h3xpepeuend5rtmurekfmnqsq6kva9yr4k3dtplku9v6qqyxr5ep6lls3hvrqyt9y7htaz9qj",
"bob_offer_priv_key": "12afb8248c7336e6aea5fe247bc4bac5dcabfb6017bd67b32c8195a6c56b8333",
"bob_offer_node_id": "035e4d1b7237898390e7999b6835ef83cd93b98200d599d29075b45ab0fedc2b34",
"contact_secret": "810641fab614f8bc1441131dc50b132fd4d1e2ccd36f84b887bbab3a6d8cc3d8"
},
{
"comment": "derive deterministic contact_secret when one offer uses both blinded paths and issuer_id",
"alice_offer": "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcsesp0grlulxv3jygx83h7tghy3233sqd6xlcccvpar2l8jshxrtwvtcsrejlwh4vyz70s46r62vtakl4sxztqj6gxjged0wx0ly8qtrygufcsyq5agaes6v605af5rr9ydnj9srneudvrmc73n7evp72tzpqcnd28puqr8a3wmcff9wfjwgk32650vl747m2ev4zsjagzucntctlmcpc6vhmdnxlywneg5caqz0ansr45z2faxq7unegzsnyuduzys7kzyugpwcmhdqqj0h70zy92p75pseunclwsrwhaelvsqy9zsejcytxulndppmykcznn7y5h",
"alice_offer_priv_key": "4ed1a01dae275f7b7ba503dbae23dddd774a8d5f64788ef7a768ed647dd0e1eb",
"alice_offer_node_id": "0284c9c6f04487ac22710176377680127dfcf110aa0fa8186793c7dd01bafdcfd9",
"bob_offer": "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcsesp0grlulxv3jygx83h7tghy3233sqd6xlcccvpar2l8jshxrtwvtcsz4n88s74qhussxsu0vs3c4unck4yelk67zdc29ree3sztvjn7pc9qyqlcpj54jnj67aa9rd2n5dhjlxyfmv3vgqymrks2nf7gnf5u200mn5qrxfrxh9d0ug43j5egklhwgyrfv3n84gyjd2aajhwqxa0cc7zn37sncrwptz4uhlp523l83xpjx9dw72spzecrtex3ku3h3xpepeuend5rtmurekfmnqsq6kva9yr4k3dtplku9v6qqyxr5ep6lls3hvrqyt9y7htaz9qjzcssy065ctv38c5h03lu0hlvq2t4p5fg6u668y6pmzcg64hmdm050jxx",
"bob_offer_priv_key": "bcaafa8ed73da11437ce58c7b3458567a870168c0da325a40292fed126b97845",
"bob_offer_node_id": "023f54c2d913e2977c7fc7dfec029750d128d735a39341d8b08d56fb6edf47c8c6",
"contact_secret": "4e0aa72cc42eae9f8dc7c6d2975bbe655683ada2e9abfdfe9f299d391ed9736c"
}
]- lightning-kmp: ACINQ/lightning-kmp#719
- phoenix wallet: ACINQ/phoenix#645