author: akumaigorodski
author: hampus_s
discussion: https://t.me/lnurl/14436
author: fiatjaf
author: dpad85
discussion: https://t.me/lnurl/24003
discussion: https://github.com/fiatjaf/lnurl-rfc/pull/101
The idea here is that the payer can identify itself when paying. The scope varies from free-form names (or LUD-16 internet identifiers) useful for loose identification in comments or similar things, to cryptographic keys for proof of payment, to authentication keys for future login into a SERVICE
's website after a payment and other use cases.
Aside from that, the payer ids are also committed to the invoice by means of the descriptionHash
property, which ensures strong cryptographic guarantees for proof of payment.
If SERVICE
wants to get one or more types of payer identities from WALLET
then it MUST alter its JSON response to the first callback to include a payerData
field, as follows (notice that the payerData
record below has a bunch of fields only for completion, an actual response will likely contain just a subset of these):
{
"callback": String,
"maxSendable": number,
"minSendable": number,
"metadata": string,
+ "payerData": {
+ "name": { "mandatory": boolean },
+ "pubkey": { "mandatory": boolean },
+ "identifier": { "mandatory": boolean },
+ "email": { "mandatory": boolean },
+ "auth": {
+ "mandatory": boolean,
+ "k1": string // hex encoded 32 bytes of challenge
+ },
+ ...other fields may be negotiated
+ },
"tag": "payRequest",
}
Notice that just including the payer id kind ("name", "pubkey" etc.) in the payerData
record is enough to signal acceptance of that kind.
In response to seeing a payerData
record in the initial response from SERVICE
, WALLET
attaches a payerdata
query parameter to LNURL-PAY callback with value set to a JSON object:
- <callback><?|&>amount=<milliSatoshi>
+ <callback><?|&>amount=<milliSatoshi>&payerdata=<urlencode({json object})>
The JSON object MUST be of the following format (notice that these fields are shown only for completion, in practice it will likely contain just a subset of these):
{
"name": string, // free form string
"pubkey": string, // hex(<randomly generated secp256k1 pubkey>),
"auth": {
"key": string, // hex(<linkingKey>)
"k1": string, // same as received from service on section 1
"sig": string, // following LUD-04: hex(sign(hexToBytes(<k1>), <linkingPrivKey>))
},
"email": string,
"identifier": string,
...other fields may be included if supported by wallet and requested by service
}
Each key in this JSON object should correspond to a requested payerdata from the payerData
record received from SERVICE
.
WALLET
CAN send any of the payer id kinds if they are listed in the payerData
record. But if any is marked as "mandatory": true
then WALLET
MUST send or otherwise do not proceed with the payment flow.
WALLET
SHOULD NOT send payer identity types omitted in payerData
record, none at all if record is not present.
If SERVICE
requests (section 1) and WALLET
sends a payerdata
record in the callback (section 2), the payer ids MUST be committed to the metadata before the invoice is created.
SERVICE
MUST append it to the metadata as it was received (after url-decoding), as in the following example:
[["text/plain", "description"], ["image/png;base64", "AAA=="]]
+[["text/plain", "description"], ["image/png;base64", "AAA=="]]{<identity records>}
After doing that, SERVICE
proceeds to hash the metadata and include that hash in the generated BOLT11 invoice as the descriptionHash
field.
On its own side, WALLET
MUST do the same thing before hashing the metadata and checking its hashed value against the descriptionHash
field of the invoice received from SERVICE
.
Even if WALLET
sends more payerdata
fields than requested by SERVICE
, SERVICE
MUST still append everything as received.
WALLET
MUST NOT expect SERVICE
to append the identity records to the metadata if SERVICE
hadn't included the payerData
record in the first response, as that would mean SERVICE
doesn't understand LUD-18 at all.
originalServiceMetadata = '[["text/plain", "description"], ["image/png;base64", "AAA=="]]'
urlEncodedPayerIds = '%7B%22name%22%3A%22bob%22%2C%22auth%22%3A%7B%22key%22%3A%2202c9323d02fc164f89c8f688dbfba8aad69a96fa8f6253ba8cce2c6f1546073fa3%22%2C%22sig%22%3A%222afd21794e2a801d0d516584ceebe1a24ed8991dd5ec708259aeaee5c0d2d1437542b689ee5d39e619a01a257142d49c18a4af3088c46ce87e2d941a1bcc7210%22%7D%2C%22identifier%22%3A%22bob%40bob.com%22%2C%22pubkey%22%3A%2203ee58475055820fbfa52e356a8920f62f8316129c39369dbdde3e5d0198a9e315%22%7D'
payerData = urldecode(urlEncodedPayerIds)
descriptionToBeHashed = metadata + payerData
descriptionToBeHashed == '[["text/plain", "description"], ["image/png;base64", "AAA=="]]{"name":"bob","auth":{"key":"02c9323d02fc164f89c8f688dbfba8aad69a96fa8f6253ba8cce2c6f1546073fa3","sig":"2afd21794e2a801d0d516584ceebe1a24ed8991dd5ec708259aeaee5c0d2d1437542b689ee5d39e619a01a257142d49c18a4af3088c46ce87e2d941a1bcc7210"},"identifier":"[email protected]","pubkey":"03ee58475055820fbfa52e356a8920f62f8316129c39369dbdde3e5d0198a9e315"}'
descriptionHash = sha256(utf8(descriptionToBeHashed))