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

blip-0042: Bolt 12 Contacts #42

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ For more detail on the process, please read [bLIP-0001](./blip-0001.md) and
| [17](./blip-0017.md) | Hosted Channels | Anton Kumaigorodskiy | Active |
| [25](./blip-0025.md) | Forward less than onion value | Valentine Wallace | Active |
| [32](./blip-0032.md) | Onion Message DNS Resolution | Matt Corallo | Active |

| [42](./blip-0042.md) | Bolt 12 Contacts | Bastien Teinturier | Active |
12 changes: 12 additions & 0 deletions blip-0002.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ network split.
* [`ping`](#ping)
* [`update_add_htlc`](#update_add_htlc)
* [Onion Messages](#onion-messages)
* [`invoice_request`](#invoice_request)

### Feature bits

Expand Down Expand Up @@ -131,6 +132,17 @@ The following table contains tlv fields for use in onion messages as the payload
| 65536 | `dnssec_query` | [bLIP 32](./blip-0032.md) |
| 65538 | `dnssec_proof` | [bLIP 32](./blip-0032.md) |

#### `invoice_request`

The following table contains extension tlv fields for `invoice_request`s sent in
onion messages:

| Type | Name | Link |
|------------|-----------------------------|---------------------------|
| 2000001729 | `invreq_contact_secret` | [bLIP 42](./blip-0042.md) |
| 2000001731 | `invreq_payer_offer` | [bLIP 42](./blip-0042.md) |
| 2000001733 | `invreq_payer_bip_353_name` | [bLIP 42](./blip-0042.md) |

## Copyright

This bLIP is licensed under the CC0 license.
261 changes: 261 additions & 0 deletions blip-0042.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
```
bLIP: 42
Title: Bolt 12 Contacts
Status: Active
Author: Bastien Teinturier <[email protected]>
Created: 2024-07-19
License: CC0
```

## Abstract

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.

## Copyright

This bLIP is licensed under the CC0 license.

## Motivation

This feature provides a better UX for lightning wallets, by making payments
between contacts look very similar to fiat payment applications.

## Specification

### Invoice Request TLVs

The `invreq_contact_secret` field is an identifier for a contact pair:

1. type: 2000001729 (`invreq_contact_secret`)
2. 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:

1. type: 2000001731 (`invreq_payer_offer`)
2. 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:

1. type: 2000001733 (`invreq_payer_bip_353_name`)
2. data:
- [`u8`:`name_len`]
- [`name_len*byte`:`name`]
- [`u8`:`domain_len`]
- [`domain_len*byte`:`domain`]

#### Requirements

The writer of `invoice_request`:

- If they want the recipient to be able to identify who paid them:
- If the recipient is not yet part of their contacts list:
- If they have previously received a payment from this recipient including
the `invreq_contact_secret` field:
- MUST associate the received `invreq_contact_secret` with this contact.
- MUST include this `invreq_contact_secret` whenever paying this contact.
- Otherwise:
- MUST generate a unique `invreq_contact_secret` for that contact.
- MUST associate this `invreq_contact_secret` with this contact.
- MUST include this `invreq_contact_secret` whenever paying this contact.
- Otherwise:
- MUST include the `invreq_contact_secret` associated with this contact.
- MUST include either `invreq_payer_offer` or `invreq_payer_bip_353_name`.
- If it includes `invreq_payer_bip_353_name`:
- MUST set `name` to the post-₿, pre-@ part of the BIP 353 HRN.
- MUST set `domain` to the post-@ part of the BIP 353 HRN.
- If it includes `invreq_payer_offer`:
- MUST encode `payer_offer` as 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_name` instead.
Comment on lines +86 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably make this verifiable somehow, no? IMO it should not be the case that I can pretend to be someone else when paying. We could include a signature by the offer's signing key, I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that would fix anything: if I'm pretending to be someone else when paying you, the offer I would include in invreq_payer_offer would also be one of mine, so I'd be able to sign the invalid bip_353_name and you wouldn't be able to tell?

I think this scheme has to rely on information obtained outside of the protocol: whenever you add someone to your contacts list based on a payment you received from them, you must have verified somehow (e.g. in real-life or through some secure messaging) that this was indeed their payment, in which case you can trust that the invreq_payer_offer and inreq_payer_bip_353_name correctly belong to them?

Copy link
Contributor

@TheBlueMatt TheBlueMatt Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that would fix anything: if I'm pretending to be someone else when paying you, the offer I would include in invreq_payer_offer would also be one of mine, so I'd be able to sign the invalid bip_353_name and you wouldn't be able to tell?

No I was thinking you'd have to sign using the offer signing key from the BIP 353 entry. That'd be verifiable.

We don't need to make the offer verifiable, necessarily, because people don't identify themselves by the offer signing key, though it would be kinda nice imo if they were.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I was thinking you'd have to sign using the offer signing key from the BIP 353 entry. That'd be verifiable.

Got it, that would make sense, as it would allow reconciling payments based on BIP 353 HRNs.

What do you think of the following proposal: if the sender of invoice_request includes invreq_bip_353_name, it MUST set invreq_payer_id to the signing key of the BIP 353 offer. This way the invoice_request is signed by the BIP 353 offer signing key. Since the payer wants to reveal their identity, I don't see any drawback in using the payer_id for this?

The reason I'd like to avoid adding an additional signature field is because:

  • we don't want to resolve the BIP 353 DNS record when receiving invoice_request: we haven't been paid yet, so it would be a DoS vector (anyone could flood us with invoice_request and ignore the invoice), we only want to resolve it after receiving the payment
  • if we added a new signature field, we would then need to include it in the path_id in the invoice returned, to be able to verify it when receiving the payment, which uses an additional 64 bytes in a potentially already large path_id (constrained by the 1300 bytes onion size)
  • whereas we already need to include the invreq_payer_id in that path_id (to allow the payer to later provide a PoP), so we only need to check that it matches the BIP 353 offer signing key after receiving the payment

If you think we should still create a new field for that signature, then we may need to require BIP 353 names to be short, probably not more than 200 characters: in practice it should already be the case, but in theory it can be 2*255 characters long...am I over-thinking this when considering that we need to handle "large" BIP 353 names?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheBlueMatt could you take a look at my last comments? I'd like to finalize a prototype version of this, and this seems to be the main blocking point that would create backwards-compat issues!

- Otherwise:
- MUST NOT include `invreq_contact_secret`, `invreq_payer_offer` or
`invreq_bip_353_name`.

The reader of `invoice_request`:

- MUST send back an `invoice` including the `invoice_request` contact fields
provided by the sender, as specified in Bolt 12.
- After the invoice has been paid, if `invreq_contact_secret` was included:
- If it matches one of their contacts:
- SHOULD display the `invreq_payer_note`, if one is provided.
- MUST ignore `invreq_payer_offer` and `invreq_bip_353_name`.
- Otherwise:
t-bast marked this conversation as resolved.
Show resolved Hide resolved
- MAY use the `contact_secret`, `payer_offer` and `payer_bip_353_name` to
create a new contact. If they do:
- MUST use the received `contact_secret` whenever paying that contact.
- MUST use the received `payer_offer` whenever paying that contact.
- If `payer_bip_353_name` was included:
- SHOULD use it to fetch a `payer_offer` if none was included.
- SHOULD use it to refresh the `payer_offer` if it expires.
- MAY use it to refresh the `payer_offer` periodically.
- MAY manually associate the received `contact_secret` with an existing
contact, if the user verified that the payment came from this contact.

t-bast marked this conversation as resolved.
Show resolved Hide resolved
#### Rationale

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](#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 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_note`s
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.

### Contact Secrets

The main mechanism of this proposal is the exchange of `contact_secret`s.
This section details various scenarios that may occur and how to correctly
deal with each of them, along with a recommended UX.

#### Adding contacts

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 random `contact_secret`. For all future payments
made to Bob where she wants to reveal that she's the payer, Alice will include
this same `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
random `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 first `contact_secret` used, 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 secondary `contact_secret`s
that our contact may use when paying us, obtained by manually associating
payments with our existing contact.

#### Leaked contact secrets

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.

### Deterministic derivation

When creating a new contact, we recommend using the following deterministic
derivation for the `contact_secret` field:

- For a given Bolt 12 offer, we define its `offer_node_id` as:
- If the offer contains `offer_issuer_id`:
- `offer_node_id = offer_issuer_id`.
- Otherwise, the offer must contain `offer_paths`:
- `offer_node_id` is set to the last `blinded_node_id` of the first
`path`.
Comment on lines +206 to +210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just say the key used to sign the invoice?

Copy link
Contributor Author

@t-bast t-bast Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That wouldn't cover all cases, because when you have multiple blinded paths and no offer_issuer_id, the key you use to sign the invoice depends on the blinded path that was used for the invoice_request. So if Alice is trying to pay Bob, and Alice has multiple blinded paths in her own offer, she wouldn't know which local key to use in the ECDH (since she's the one sending invoice_request, not receiving it)?

That's why I'm explicitly specifying that when multiple blinded paths are used, we only use the first one for this contact_secret derivation. Does that make sense?

- The private key for the `offer_node_id` is called `offer_priv_key`.
- When paying `remote_offer` for which we include our `local_offer` in the
`invreq_payer_offer` field:
- We compute the ECDH of the two `offer_node_id`s:
- `shared_key = local_offer.offer_priv_key * remote_offer.offer_node_id`.
TheBlueMatt marked this conversation as resolved.
Show resolved Hide resolved
- We use a tagged hash to derive the `contact_secret`:
- `contact_secret = SHA256("blip42_contact_secret" || shared_key)`.

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.

#### Test vector

The following test vectors use the deterministic derivation from the previous
section.

```json
[
{
"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"
}
]
```

## Reference Implementations

- lightning-kmp: <https://github.com/ACINQ/lightning-kmp/pull/719>
- phoenix wallet: <https://github.com/ACINQ/phoenix/pull/645>