diff --git a/12-offer-encoding.md b/12-offer-encoding.md index 3a3114074..79c524d5d 100644 --- a/12-offer-encoding.md +++ b/12-offer-encoding.md @@ -83,7 +83,14 @@ come). There is no checksum, unlike bech32m. ## Requirements +Writers of a bolt12 string: +- MUST either use all lowercase or all UPPERCASE. +- SHOULD use uppercase for QR codes. +- SHOULD use lower case otherwise. +- MAY use `+`, optionally followed by whitespace, to separate large bolt12 strings. + Readers of a bolt12 string: +- MUST handle strings which are all lowercase, or all uppercase. - if it encounters a `+` followed by zero or more whitespace characters between two bech32 characters: - MUST remove the `+` and whitespace. @@ -231,11 +238,11 @@ A writer of an offer: - if the chain for the invoice is not solely bitcoin: - MUST specify `offer_chains` the offer is valid for. - otherwise: - - MAY omit `offer_chains`, implying that bitcoin is only chain. + - SHOULD omit `offer_chains`, implying that bitcoin is only chain. - if a specific minimum `offer_amount` is required for successful payment: - MUST set `offer_amount` to the amount expected (per item). - if the currency for `offer_amount` is that of all entries in `chains`: - - MUST specify `amount` in multiples of the minimum lightning-payable unit + - MUST specify `offer_amount` in multiples of the minimum lightning-payable unit (e.g. milli-satoshis for bitcoin). - otherwise: - MUST specify `offer_currency` `iso4217` as an ISO 4712 three-letter code. @@ -260,7 +267,6 @@ A writer of an offer: - otherwise: - MAY include `offer_paths`. - if it includes `offer_paths`: - - SHOULD ignore any invoice_request which does not use the path. - MAY set `offer_issuer_id`. - otherwise: - MUST set `offer_issuer_id` to the node's public key to request the invoice from. @@ -294,6 +300,8 @@ A reader of an offer: - MUST NOT respond to the offer - if `offer_amount` is set and `offer_description` is not set: - MUST NOT respond to the offer. + - if `offer_amount` is set and `offer_currency` is not set: + - MUST NOT respond to the offer. - if neither `offer_issuer_id` nor `offer_paths` are set: - MUST NOT respond to the offer. - if `num_hops` is 0 in any `blinded_path` in `offer_paths`: @@ -303,10 +311,10 @@ A reader of an offer: - `offer_currency` field if set - otherwise, the minimum lightning-payable unit (e.g. milli-satoshis for bitcoin). - - MUST warn user if amount of actual invoice differs significantly + - MUST warn the user if the received `invoice_amount` differs significantly from that estimate. - - SHOULD not respond to an offer if the current time is after - `offer_absolute_expiry`. + - if the current time is after `offer_absolute_expiry`: + - MUST NOT respond to the offer. - if it chooses to sends an `invoice_request`, it sends an onion message: - if `offer_paths` is set: - MUST send the onion message via any path in `offer_paths` to the final `onion_msg_hop`.`blinded_node_id` in that path @@ -439,7 +447,7 @@ The writer: - if it is responding to an offer: - MUST copy all fields from the offer (including unknown fields). - if `offer_chains` is set: - - MUST set `invreq_chain` to one of `offer_chains` unless that chain is bitcoin, in which case it MAY omit `invreq_chain`. + - MUST set `invreq_chain` to one of `offer_chains` unless that chain is bitcoin, in which case it SHOULD omit `invreq_chain`. - otherwise: - if it sets `invreq_chain` it MUST set it to bitcoin. - MUST set `signature`.`sig` as detailed in [Signature Calculation](#signature-calculation) using the `invreq_payer_id`. @@ -490,6 +498,10 @@ The reader: - MUST NOT reply with a previous invoice. - if `offer_issuer_id` or `offer_paths` are present (response to an offer): - MUST fail the request if the offer fields do not exactly match a valid, unexpired offer. + - if `offer_paths` is present: + - MUST ignore the invoice_request if it did not arrive via one of those paths. + - otherwise: + - MUST ignore any invoice_request if it arrived via a blinded path. - if `offer_quantity_max` is present: - MUST fail the request if there is no `invreq_quantity` field. - if `offer_quantity_max` is non-zero: @@ -545,10 +557,12 @@ so `offer_amount` and `offer_currency` are redundant (but may be informative for the payer to know how the sender claims `invreq_amount` was derived). +The requirement to use `offer_paths` if present, ensures a node does not reveal it is the source of an offer if it is asked directly. Similarly, the requirement that the correct path is used for the offer ensures that cannot be made to reveal that it is the same node that created some other offer. + # Invoices Invoices are a payment request, and when the payment is made, -it can be combined with the invoice to form a cryptographic receipt. +the payment preimage can be combined with the invoice to form a cryptographic receipt. The recipient sends an `invoice` in response to an `invoice_request` using the `onion_message` `invoice` field. @@ -705,7 +719,7 @@ A writer of an invoice: seconds after `invoice_created_at` that payment of this invoice should not be attempted. - if it accepts onchain payments: - MAY specify `invoice_fallbacks` - - MUST specify `invoice_fallbacks` in order of most-preferred to least-preferred + - SHOULD specify `invoice_fallbacks` in order of most-preferred to least-preferred if it has a preference. - for the bitcoin chain, it MUST set each `fallback_address` with `version` as a valid witness version and `address` as a valid witness @@ -752,11 +766,18 @@ A reader of an invoice: - MAY pay the invoice via multiple separate payments. - otherwise: - MUST NOT use multiple parts to pay the invoice. - - SHOULD confirm authorization if `invoice_amount`.`msat` is not within the amount range authorized. + - if `invreq_amount` is present: + - MUST reject the invoice if `invoice_amount` is not equal to `invreq_amount` + - otherwise: + - SHOULD confirm authorization if `invoice_amount`.`msat` is not within the amount range authorized. - for the bitcoin chain, if the invoice specifies `invoice_fallbacks`: - MUST ignore any `fallback_address` for which `version` is greater than 16. - MUST ignore any `fallback_address` for which `address` is less than 2 or greater than 40 bytes. - MUST ignore any `fallback_address` for which `address` does not meet known requirements for the given `version` + - if `invreq_paths` is present: + - MUST ignore the invoice if it did not arrive via one of those paths. + - otherwise: + - MUST ignore any invoice if it arrived via a blinded path. ## Rationale diff --git a/bolt12/format-string-test.json b/bolt12/format-string-test.json index e97cb5b19..2bbd7d214 100644 --- a/bolt12/format-string-test.json +++ b/bolt12/format-string-test.json @@ -4,6 +4,11 @@ "valid": true, "string": "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg" }, + { + "comment": "Uppercase is valid", + "valid": true, + "string": "LNO1PQPS7SJQPGTYZM3QV4UXZMTSD3JJQER9WD3HY6TSW35K7MSJZFPY7NZ5YQCNYGRFDEJ82UM5WF5K2UCKYYPWA3EYT44H6TXTXQUQH7LZ5DJGE4AFGFJN7K4RGRKUAG0JSD5XVXG" + }, { "comment": "+ can join anywhere", "valid": true, @@ -19,6 +24,11 @@ "valid": true, "string": "lno1pqps7sjqpgt+ yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+ 5k7msjzfpy7nz5yqcn+\nygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+\r\n 5xvxg" }, + { + "comment": "+ can be followed by whitespace, UPPERCASE", + "valid": true, + "string": "LNO1PQPS7SJQPGT+ YZM3QV4UXZMTSD3JJQER9WD3HY6TSW3+ 5K7MSJZFPY7NZ5YQCN+\nYGRFDEJ82UM5WF5K2UCKYYPWA3EYT44H6TXTXQUQH7LZ5DJGE4AFGFJN7K4RGRKUAG0JSD+\r\n 5XVXG" + }, { "comment": "+ must be surrounded by bech32 characters", "valid": false, diff --git a/bolt12/offers-test.json b/bolt12/offers-test.json index ac7e318b2..891ed5673 100644 --- a/bolt12/offers-test.json +++ b/bolt12/offers-test.json @@ -333,6 +333,29 @@ } ] }, + { + "description": "same, with blinded path first_node_id using sciddir", + "valid": true, + "bolt12": "lno1pgx9getnwss8vetrw3hhyucs3yqqqqqqqqqqqqp2qgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqqyqqqqqqqqqqqqqqqqqqqqqqqqqqqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqqgzyg3zyg3zyg3z93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj", + "field info": "short_channel_id is 0x0x42, direction is 0", + "fields": [ + { + "type": 10, + "length": 12, + "hex": "5465737420766563746f7273" + }, + { + "type": 16, + "length": 137, + "hex": "00000000000000002a0202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200100000000000000000000000000000000002020202020202020202020202020202020202020202020202020202020202020200081111111111111111" + }, + { + "type": 22, + "length": 33, + "hex": "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" + } + ] + }, { "description": "with no issuer_id and blinded path via Bob (0x424242...), blinding 020202...", "valid": true, @@ -541,17 +564,17 @@ "bolt12": "lno1pgz5znzfgdz3vggzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgp06wu6egp9qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq" }, { - "description": "Contains unknown feature 22", + "description": "Contains unknown feature 122", "valid": false, - "bolt12": "lno1pgx9getnwss8vetrw3hhyucvqdqqqqqkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg" + "bolt12": "lno1pgx9getnwss8vetrw3hhyucvzqzqqqqqqqqqqqqqqqqqqqqqqqqpvggzamrjghtt05kvkvpcp0a79gmy3nt6jsn98ad2xs8de6sl9qmgvcvs" }, { - "description": "Missing offer_description and offer_amount", + "description": "Missing offer_description, but has offer_amount", "valid": false, - "bolt12": "lno1zcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese" + "bolt12": "lno1pqpzwyqkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg" }, { - "description": "Missing offer_issuer_id", + "description": "Missing offer_issuer_id and no offer_path", "valid": false, "bolt12": "lno1pgx9getnwss8vetrw3hhyuc" },