From b47c545b4406d028df4b10e27356821a4d9bd1fd Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 20 Mar 2020 15:08:13 +1030 Subject: [PATCH 1/9] BOLT 7: variable message onion message support. These use onion encoding for simple one-way messaging: there are no error returns. Any reply is done with an enclosed blinded path. Note that this defines the message system, not the contents of messages (e.g. invoice requests from offers). Signed-off-by: Rusty Russell --- .aspell.en.pws | 1 + 01-messaging.md | 2 +- 04-onion-routing.md | 81 ++++++++++++++++++++++++++++++++++++++++++++ 07-routing-gossip.md | 56 ++++++++++++++++++++++++++++-- 09-features.md | 1 + 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/.aspell.en.pws b/.aspell.en.pws index c9e3a0206..d247edfa7 100644 --- a/.aspell.en.pws +++ b/.aspell.en.pws @@ -386,3 +386,4 @@ csv CHECKSIGVERIFY IFDUP sats +onionmsg diff --git a/01-messaging.md b/01-messaging.md index 678393e1b..7f030d7d3 100644 --- a/01-messaging.md +++ b/01-messaging.md @@ -49,7 +49,7 @@ The messages are grouped logically into five groups, ordered by the most signifi - Setup & Control (types `0`-`31`): messages related to connection setup, control, supported features, and error reporting (described below) - Channel (types `32`-`127`): messages used to setup and tear down micropayment channels (described in [BOLT #2](02-peer-protocol.md)) - Commitment (types `128`-`255`): messages related to updating the current commitment transaction, which includes adding, revoking, and settling HTLCs as well as updating fees and exchanging signatures (described in [BOLT #2](02-peer-protocol.md)) - - Routing (types `256`-`511`): messages containing node and channel announcements, as well as any active route exploration (described in [BOLT #7](07-routing-gossip.md)) + - Routing (types `256`-`511`): messages containing node and channel announcements, as well as any active route exploration or messaging (described in [BOLT #7](07-routing-gossip.md)) - Custom (types `32768`-`65535`): experimental and application-specific messages The size of the message is required by the transport layer to fit into a 2-byte unsigned int; therefore, the maximum possible size is 65535 bytes. diff --git a/04-onion-routing.md b/04-onion-routing.md index 5677a1427..1ecfbec60 100644 --- a/04-onion-routing.md +++ b/04-onion-routing.md @@ -51,6 +51,7 @@ A node: * [Legacy HopData Payload Format](#legacy-hop_data-payload-format) * [TLV Payload Format](#tlv_payload-format) * [Basic Multi-Part Payments](#basic-multi-part-payments) + * [Onion Messages](#onion-messages) * [Accepting and Forwarding a Payment](#accepting-and-forwarding-a-payment) * [Payload for the Last Node](#payload-for-the-last-node) * [Non-strict Forwarding](#non-strict-forwarding) @@ -367,6 +368,86 @@ otherwise meets the amount criterion (eg. some other failure, or invoice timeout), however if it were to fulfill only some of them, intermediary nodes could simply claim the remaining ones. +### Onion Messages + +Onion messages have an onion with an alternate `hop_payload` +format: a `bigsize` followed by a `onionmsg_payload`. Note that there +is no legacy format, thus a `bigsize` of 0 means no payload. + +1. tlvs: `onionmsg_payload` +2. types: + 1. type: 4 (`next_node_id`) + 2. data: + * [`point`:`node_id`] + 1. type: 6 (`next_short_channel_id`) + 2. data: + * [`short_channel_id`:`short_channel_id`] + 1. type: 8 (`reply_path`) + 2. data: + * [`point`:`blinding`] + * [`...*onionmsg_path`:`path`] + 1. type: 10 (`enctlv`) + 2. data: + * [`...*byte`:`enctlv`] + 1. type: 12 (`blinding`) + 2. data: + * [`point`:`blinding`] + +1. tlvs: `encmsg_tlvs` +2. types: + 1. type: 4 (`next_node_id`) + 2. data: + * [`point`:`node_id`] + 1. type: 6 (`next_short_channel_id`) + 2. data: + * [`short_channel_id`:`short_channel_id`] + +1. subtype: `onionmsg_path` +2. data: + * [`point`:`node_id`] + * [`u16`:`enclen`] + * [`enclen*byte`:`enctlv`] + +#### Requirements + +The writer: +- For the non-final nodes' `onionmsg_payload`: + - MUST include exactly one of `next_short_channel_id`, `next_node_id` + or `enctlv` indicating the next node. +- For the final node's `onionmsg_payload`: + - if the final node is permitted to reply: + - MUST set `reply_path` `blinding` to the initial blinding factor for the `next_node_id` + - For the first `reply_path` `path`: + - MUST set `node_id` to the first node in the reply path. + - For the remaining `reply_path` `path`: + - MUST set `node_id` to the blinded node id to encrypt the onion hop for. + - Within `reply_path` `path`: + - MUST encrypt `enctlv` as detailed in (FIXME: reference to t-bast's blinded path section: + `ChaChaPoly-1305` encryption using an all-zero nonce). + - MUST set `enctlv` to a valid `encmsg_tlvs` containing exactly one of either + `next_node_id` or `next_short_channel_id`. + - otherwise: + - MUST NOT set `reply_path`. + +The reader: +- if `enctlv` is present: + - MUST extract the shared secret from the given `blinding` parameter and decrypt `enctlv`. + - MUST drop the message if `enctlv` is not a valid TLV. + - MUST use `next_short_channel_id` or `next_node_id` from `enctlv`. +- Otherwise: + - MUST use `next_short_channel_id` or `next_node_id` from `onionmsg_payload`. + +- if it is not the final node according to the onion encryption: + - if `next_short_channel_id` or `next_node_id` is found: + - SHOULD forward the message using `onion_message` to the indicated peer if possible. + +- otherwise: + - if it wants to send a reply: + - MUST create an onion encoding using `reply_path`. + - MUST send the reply via `onion_message` to the node indicated by + the first element of `reply_path` `path` using `reply_path` `blinding`. + + # Accepting and Forwarding a Payment Once a node has decoded the payload it either accepts the payment locally, or forwards it to the peer indicated as the next hop in the payload. diff --git a/07-routing-gossip.md b/07-routing-gossip.md index b89c3d222..10eee0690 100644 --- a/07-routing-gossip.md +++ b/07-routing-gossip.md @@ -1,4 +1,4 @@ -# BOLT #7: P2P Node and Channel Discovery +# BOLT #7: P2P Node and Channel Discovery and Onion Messages This specification describes simple node discovery, channel discovery, and channel update mechanisms that do not rely on a third-party to disseminate the information. @@ -33,6 +33,7 @@ To support channel and node discovery, three *gossip messages* are supported: * [HTLC Fees](#htlc-fees) * [Pruning the Network View](#pruning-the-network-view) * [Recommendations for Routing](#recommendations-for-routing) + * [Onion Messages](#onion-messages) * [References](#references) ## Definition of `short_channel_id` @@ -1119,7 +1120,58 @@ A->D's `update_add_htlc` message would be: And D->C's `update_add_htlc` would again be the same as B->C's direct payment above. -## References +# Onion Messages + +Onion messages allow peers to use existing connections to query for +invoices (see [BOLT 12](12-offer-encoding.md)). Like gossip messages, +they are not associated with a particular local channel. Like HTLCs, +they use [BOLT 4](04-onion-routing.md#onion-messages) protocol for +end-to-end encryption. + +Onion messages are unreliable: in particular, they are designed to +be cheap to process and require no storage to forward. As a result, +there is no error returned from intermediary nodes. + +To enable messaging via blinded paths, there is an optional `blinding` +parameter which allows decryption of the `enctlv` field inside the +`onionmsg`'s `onionmsg_payload`. + +## The `onion_message` Message + +1. type: 385 (`onion_message`) (`option_onion_messages`) +2. data: + * [`u16`:`len`] + * [`len*byte`:`onionmsg`] + * [`onion_message_tlvs`:`onion_message_tlvs`] + +1. tlvs: `onion_message_tlvs` +2. types: + 1. type: 2 (`blinding`) + 2. data: + * [`point`:`blinding`] + +## Requirements + +The writer: +- MUST populate the per-hop payloads as described in [BOLT 4](04-onion-routing.md#onion-messages). +- SHOULD retry via a different route if it expects a response and + doesn't receive one after a reasonable period. +- SHOULD set `len` to 1366 or 32834. + +The reader: +- MUST handle the per-hop payloads as described in [BOLT 4](04-onion-routing.md#onion-messages). +- SHOULD accept onion messages from peers without an established channel. +- MAY rate-limit messages by dropping them. + +## Rationale + +`len` allows larger messages to be sent than the standard 1300 bytes +allowed for an HTLC onion, but this should be used sparingly as it is +reduces anonymity set, hence the recommendation that it either look +like an HTLC onion, or if larger, be a fixed size. + + +# References 1. [RFC 1950 "ZLIB Compressed Data Format Specification version 3.3](https://www.ietf.org/rfc/rfc1950.txt) 2. [Maximum Compression Factor](https://zlib.net/zlib_tech.html) diff --git a/09-features.md b/09-features.md index ea37b8b5e..a9b0d1ed5 100644 --- a/09-features.md +++ b/09-features.md @@ -40,6 +40,7 @@ The Context column decodes as follows: | 18/19 | `option_support_large_channel` | Can create large channels | IN | | [BOLT #2](02-peer-protocol.md#the-open_channel-message) | | 20/21 | `option_anchor_outputs` | Anchor outputs | IN | `option_static_remotekey` | [BOLT #3](03-transactions.md) | | 22/23 | `option_anchors_zero_fee_htlc_tx` | Anchor commitment type with zero fee HTLC transactions | IN | | [BOLT #3][bolt03-htlc-tx], [lightning-dev][ml-sighash-single-harmful]| +| 102/103 | `option_onion_messages` | Can forward onion messages | IN9 | | [BOLT #7](07-routing-gossip.md#onion-messages) | ## Requirements From 1006d88353d56dc12da5f76b5fe7a3cd8a705459 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 16 Apr 2021 14:46:02 +0930 Subject: [PATCH 2/9] BOLT 4: More notes on blinding, and a general matching requirement. If we supply a reply_path, we *must not* respond if we get a message which doesn't use it. Similarly, we must not respond to requests via a reply_path except the exact reply we want. --- 07-routing-gossip.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/07-routing-gossip.md b/07-routing-gossip.md index 10eee0690..5f8f46690 100644 --- a/07-routing-gossip.md +++ b/07-routing-gossip.md @@ -1159,12 +1159,24 @@ The writer: - SHOULD set `len` to 1366 or 32834. The reader: +- MUST ignore any message which contains a `blinding` which it did not expect, or does not contain + a `blinding` when one is expected. - MUST handle the per-hop payloads as described in [BOLT 4](04-onion-routing.md#onion-messages). - SHOULD accept onion messages from peers without an established channel. - MAY rate-limit messages by dropping them. ## Rationale +`blinding` is critical to the use of blinded paths: there are various +means by which a blinded path is passed to a node. The receipt of an +expected `blinding` indicates that blinded path has been used: it is +important that a node not accept unblinded messages when it is expecting +a blinded message, as this implies the sender is probing to detect if +the recipient is the terminus of the blinded path. + +Similarly, since blinded paths don't expire, a node could try to use +a blinded path to send an unexpected message hoping for a response. + `len` allows larger messages to be sent than the standard 1300 bytes allowed for an HTLC onion, but this should be used sparingly as it is reduces anonymity set, hence the recommendation that it either look From b49e2a44418f700549032b5d820ce5fae80ea21c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 16 Apr 2021 14:54:39 +0930 Subject: [PATCH 3/9] BOLT 1: add bip340sig type. Signed-off-by: Rusty Russell --- 01-messaging.md | 1 + 1 file changed, 1 insertion(+) diff --git a/01-messaging.md b/01-messaging.md index 7f030d7d3..819f03636 100644 --- a/01-messaging.md +++ b/01-messaging.md @@ -231,6 +231,7 @@ The following convenience types are also defined: * `channel_id`: a 32-byte channel_id (see [BOLT #2](02-peer-protocol.md#definition-of-channel-id)) * `sha256`: a 32-byte SHA2-256 hash * `signature`: a 64-byte bitcoin Elliptic Curve signature +* `bip340sig`: a 64-byte bitcoin Elliptic Curve Schnorr signature as per [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) * `point`: a 33-byte Elliptic Curve point (compressed encoding as per [SEC 1 standard](http://www.secg.org/sec1-v2.pdf#subsubsection.2.3.3)) * `short_channel_id`: an 8 byte value identifying a channel (see [BOLT #7](07-routing-gossip.md#definition-of-short-channel-id)) * `bigsize`: a variable-length, unsigned integer similar to Bitcoin's CompactSize encoding, but big-endian. Described in [BigSize](#appendix-a-bigsize-test-vectors). From 669ad396a215d86a531ff7f2ada8c91f9c82b843 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 16 Apr 2021 14:56:08 +0930 Subject: [PATCH 4/9] BOLT 1: Add utf8 type. It's far easier to validate these on parsing than to hand-validate them elsewhere. I didn't turn `alias` or `error` into this, though they're similar (`alias` can have a nul terminator). Signed-off-by: Rusty Russell --- 01-messaging.md | 1 + 1 file changed, 1 insertion(+) diff --git a/01-messaging.md b/01-messaging.md index 819f03636..6c030fd3d 100644 --- a/01-messaging.md +++ b/01-messaging.md @@ -235,6 +235,7 @@ The following convenience types are also defined: * `point`: a 33-byte Elliptic Curve point (compressed encoding as per [SEC 1 standard](http://www.secg.org/sec1-v2.pdf#subsubsection.2.3.3)) * `short_channel_id`: an 8 byte value identifying a channel (see [BOLT #7](07-routing-gossip.md#definition-of-short-channel-id)) * `bigsize`: a variable-length, unsigned integer similar to Bitcoin's CompactSize encoding, but big-endian. Described in [BigSize](#appendix-a-bigsize-test-vectors). +* `utf8`: a byte as part of a UTF-8 string. A writer MUST ensure an array of these is a valid UTF-8 string, a reader MAY reject any messages containing an array of these which is not a valid UTF-8 string. ## Setup Messages From 65643ea86477033385fc1ef3de573fc8fd524294 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 16 Apr 2021 15:17:39 +0930 Subject: [PATCH 5/9] tools/spellcheck.sh: more generally ignore things inside ``. Signed-off-by: Rusty Russell --- tools/spellcheck.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/spellcheck.sh b/tools/spellcheck.sh index fb309315f..ac12eb8e9 100755 --- a/tools/spellcheck.sh +++ b/tools/spellcheck.sh @@ -63,8 +63,7 @@ do WORDS=$(sed -e 's/ [lL][nN]\([bB][cC]\|[tT][bB]\)[0-9munpxMUNP]*1[qpzry9x8gf2tvdw0s3jn54khce6mua7lQPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]\+//g' \ -e 's/\]([-#a-zA-Z0-9_.]*)//g' \ -e '/^```/,/^```/d' \ - -e 's/`[a-zA-Z0-9_]*`//g' \ - -e 's/\* \[`[_a-z0-9*]\+`://g' \ + -e 's/`[a-zA-Z0-9_*.(),]*`//g' \ -e 's/0x[a-fA-F0-9 ]\+//g' \ -e 's/[a-fA-F0-9]\{20,\}//g' \ -e 's/^ .*_htlcs//g' \ From c5646431ba5b4f9e4ffb2542437d574c76ec75f8 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 16 Apr 2021 15:18:39 +0930 Subject: [PATCH 6/9] BOLT 12: offers, third draft A BOLT11 "invoice" has proven too low-level for human use in many scenarios. Efforts like lnurl have covered the gap, but integrating some of such higher layers into the lightning protocol itself has many advantages. This draft defines three new things: 1. A new invoice format. I know, this is painful, but it maps almost 1:1 to the current format (though signatures are very different), is easier to implement, and easier to send via the lightning network itself. 2. Formats for an "offer", which for all intents and purposes serves as the new, persistent invoice for users. It can also be a "send_invoice" offer, which is an offer to send money (refund, withdrawl). 3. Format for an "invoice_request": this is a message sent via the lightning network itself to receive the real invoice. Signed-off-by: Rusty Russell --- 04-onion-routing.md | 10 + 12-offer-encoding.md | 1000 ++++++++++++++++++++++++++++++++ bolt12/format-string-test.json | 47 ++ bolt12/offer-period-test.json | 527 +++++++++++++++++ 4 files changed, 1584 insertions(+) create mode 100644 12-offer-encoding.md create mode 100644 bolt12/format-string-test.json create mode 100644 bolt12/offer-period-test.json diff --git a/04-onion-routing.md b/04-onion-routing.md index 1ecfbec60..3a066a12a 100644 --- a/04-onion-routing.md +++ b/04-onion-routing.md @@ -392,6 +392,16 @@ is no legacy format, thus a `bigsize` of 0 means no payload. 1. type: 12 (`blinding`) 2. data: * [`point`:`blinding`] + 1. type: 64 (`invoice_request`) + 2. data: + * [`...*byte`:`invoice_request`] + 1. type: 66 (`invoice`) + 2. data: + * [`...*byte`:`invoice`] + 1. type: 68 (`invoice_error`) + 2. data: + * [`...*byte`:`invoice_error`] + 1. tlvs: `encmsg_tlvs` 2. types: diff --git a/12-offer-encoding.md b/12-offer-encoding.md new file mode 100644 index 000000000..0cf403935 --- /dev/null +++ b/12-offer-encoding.md @@ -0,0 +1,1000 @@ +# BOLT #12: Flexible Protocol for Lightning Payments + +# Table of Contents + + * [Limitations of BOLT 11](#limitations-of-bolt-11) + * [Payment Flow Scenarios](#payment-flow-scenarios) + * [Encoding](#encoding) + * [TLV Fields](#tlv-fields) + * [Invoices](#invoices) + * [Offers](#offers) + * [Invoice Requests](#invoice-requests) + * [Invoice Errors](#invoice-errors) + +# Limitations of BOLT 11 + +The BOLT 11 invoice format has proven popular, but has several +limitations: + +1. The entangling of bech32 encoding makes it awkward to send + in other forms (i.e. inside the lightning network itself). +2. The signature applying to the entire invoice makes it impossible + to prove an invoice without revealing its entirety. +3. Fields are not generally extractable for external use: the `h` + field was a boutique extraction of the `d` field, only. +4. The lack of 'it's ok to be odd' rule makes backwards compatibility + harder. +5. The 'human-readable' idea of separating amounts proved fraught: + `p` was often mishandled, and amounts in pico-bitcoin are harder + than the modern satoshi-based counting. +6. The bech32 encoding was found to have an issue with extensions, + which means we want to replace or discard it anyway. +7. The `payment_secret` designed to prevent probing by other nodes in + the path was only useful if the invoice remained private between the + payer and payee. +8. Invoices must be given per-user, and are actively dangerous if two + payment attempts are made for the same user. + + +# Payment Flow Scenarios + +Here we use "user" as shorthand for the individual user's lightning +node, and "merchant" as the shorthand for the node of someone who is +selling or has sold something. + +There are two basic payment flows supported by BOLT 12: + +The general user-pays-merchant flow is: +1. A merchant publishes an *offer* ("send me money"), such as on a web page or a QR code. +2. Every user requests a unique *invoice* over the lightning network + using an *invoice_request* message. +3. The merchant replies with the *invoice*. +4. The user makes a payment to the merchant indicated by the invoice. + +The merchant-pays-user flow (e.g. ATM or refund): +1. The merchant provides a user-specific *offer* ("take my money") in a webpage or QR code, + with an amount (for a refund, also a reference to the to-be-refunded + invoice). +2. A user sends an *invoice* for the amount in the *offer* (for a + refund, a proof that they requested the original) +3. The merchant makes a payment to the user indicated by the invoice. + +## Payment Proofs and Payer Proofs + +Note that the normal lightning "proof of payment" can only demonstrate that an +invoice was paid (by showing the preimage of the `payment_hash`), not who paid +it. The merchant can claim an invoice was paid, and once revealed, anyone can +claim they paid the invoice, too.[1] + +1. Sharing the minimum information required to prove sections of the + invoice in dispute (e.g. the description of the item, the payment + hash, and the merchant's signature). +2. Contain a transferrable proof that the user is the one who was + responsible for paying the invoice in the first place. + +Providing a key in *invoice_request* allows a user to prove that they were the one +to request the invoice. In addition, the merkle construction of the BOLT 12 +invoice signature allows the user to selectively reveal fields of the invoice +in case of dispute. + +# Encoding + +Each of the forms documented here are in +[TLV](01-messaging.md#type-length-value-format) format. + +The supported ASCII encoding is the human-readable prefix, followed by a +`1`, followed by a bech32-style data string of the TLVs in order, +optionally interspersed with `+` (for indicating additional data is to +come). + +## Requirements + +Readers of a bolt12 string: +- if it encounters a `+` followed zero or more whitespace characters between + two bech32 characters: + - MUST remove the `+` and whitespace. + +## Rationale + +The use of bech32 is arbitrary, but already exists in the bitcoin +world. We omit the six-character trailing checksum since all uses +here involve a signature. + +The use of `+` (which is ignored) allows use over limited +text fields like Twitter: + +``` +lno1xxxxxxxx+ + +yyyyyyyyyyyy+ + +zzzzz +``` + +See [format-string-test.json](bolt12/format-string-test.json). + +## Signature Calculation + +All signatures are created as per [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), and tagged as recommended +there. Thus to sign a message `msg` with `tag`, `m` is +SHA256(SHA256(`tag`) || SHA256(`tag`) || `msg`). The notation used +here is `SIG(tag,msg,key)`. + +Each form is signed using one or more TLV signature elements; TLV +types 240 through 1000 are considered signature elements. For these +the tag is `lightning` | `messagename` | `fieldname`, and `msg` is the +merkle-root; `lightning` is the literal 9-byte ASCII string, +`messagename` is the name of the TLV stream being signed (i.e. `offer` +or `invoice`) and the `fieldname` is the TLV field containing the +signature (e.g. `signature` or `recurrence_signature`). + +The formulation of the merkle tree is similar to that proposed in +[BIP-taproot], with the insertion of alternate "dummy" leaves to avoid +revealing adjacent nodes in proofs. + +The Merkle Tree's leaves are, in TLV-ascending order: +1. The SHA256 of: `LnLeaf` followed by the TLV entry. +2. The SHA256 of: `LnAll` followed all non-signature TLV entries appended in ascending order. + +The Merkle tree inner nodes are SHA256(`LnBranch` | lesser-SHA256 | +greater-SHA256); this ordering means that proofs are more compact +since left/right is inherently determined. + +If there are not exactly a power of 2 leaves, then the tree depth will +be uneven, with the deepest tree on the lowest-order leaves. + +e.g. consider the encoding of an `offer` `signature` with TLVs TLV1, TLV2 and TLV3: + +``` +LALL=SHA256(`LnAll`|TLV1|TLV2|TLV3) +L1=SHA256(`LnLeaf`|TLV1) +L2=SHA256(`LnLeaf`|TLV2) +L3=SHA256(`LnLeaf`|TLV3) + +Assume L1 < LALL, and L2 > LALL. + + L1 LALL L2 LALL L3 + \ / \ / | + v v v v v +L1A=SHA256('LnBranch'|L1|LALL) L2A=SHA256('LnBranch'|LALL|L2) L3 + +Assume L1A < L2A: + + L1A L2A L3 + \ / | + v v v + L1A2A=SHA256('LnBranch'|L1A|L2A) L3 + +Assume L1A2A > L3: + + L1A2A=SHA256('LnBranch'|L1A|L2A) L3 + \ / + v v + Root=SHA256('LnBranch'|L3|L1A2A) + +Signature = SIG('lightningoffersignature', Root, nodekey) +``` + +## Rationale + +FIXME: some taproot, some about obscuring leaves in merkle proofs. + +# Offers + +Offers are a precursor to an invoice: readers will either request an invoice +(or multiple) or send an invoice based on the offer. An offer can be much longer-lived than a +particular invoice, so has some different characteristics; in particular it +can be recurring, and the amount can be in a non-lightning currency. It's +also designed for compactness, to easily fit inside a QR code. + +The human-readable prefix for offers is `lno`. + +## TLV Fields for Offers + +1. tlvs: `offer` +2. types: + 1. type: 2 (`chains`) + 2. data: + * [`...*chain_hash`:`chains`] + 1. type: 6 (`currency`) + 2. data: + * [`...*utf8`:`iso4217`] + 1. type: 8 (`amount`) + 2. data: + * [`tu64`:`amount`] + 1. type: 10 (`description`) + 2. data: + * [`...*utf8`:`description`] + 1. type: 12 (`features`) + 2. data: + * [`...*byte`:`features`] + 1. type: 14 (`absolute_expiry`) + 2. data: + * [`tu64`:`seconds_from_epoch`] + 1. type: 16 (`paths`) + 2. data: + * [`...*blinded_path`:`paths`] + 1. type: 20 (`vendor`) + 2. data: + * [`...*utf8`:`vendor`] + 1. type: 22 (`quantity_min`) + 2. data: + * [`tu64`:`min`] + 1. type: 24 (`quantity_max`) + 2. data: + * [`tu64`:`max`] + 1. type: 26 (`recurrence`) + 2. data: + * [`byte`:`time_unit`] + * [`tu32`:`period`] + 1. type: 64 (`recurrence_paywindow`) + 2. data: + * [`u32`:`seconds_before`] + * [`byte`:`proportional_amount`] + * [`tu32`:`seconds_after`] + 1. type: 66 (`recurrence_limit`) + 2. data: + * [`tu32`:`max_period`] + 1. type: 28 (`recurrence_base`) + 2. data: + * [`byte`:`start_any_period`] + * [`tu64`:`basetime`] + 1. type: 30 (`node_id`) + 2. data: + * [`pubkey32`:`node_id`] + 1. type: 54 (`send_invoice`) + 1. type: 34 (`refund_for`) + 2. data: + * [`sha256`:`refunded_payment_hash`] + 1. type: 240 (`signature`) + 2. data: + * [`bip340sig`:`sig`] + +1. subtype: `blinded_path` +2. data: + * [`point`:`blinding`] + * [`u16`:`num_hops`] + * [`num_hops*onionmsg_path`:`path`] + +## Recurrence + +Some offers are *periodic*, such as a subscription service or monthly +dues, in that payment is expected to be repeated. There are many +different flavors of repetition, consider: + +* Payments due on the first of every month, for 6 months. +* Payments due on every Monday, 1pm Pacific Standard Time. +* Payments due once a year: + * which must be made on January 1st, or + * which are only valid if started January 1st 2021, or + * which if paid after January 1st you (over) pay the full rate first year, or + * which if paid after January 1st are paid pro-rata for the first year, or + * which repeat from whenever you made the first payment + +Thus, each payment has: +1. A `time_unit` defining 0 (seconds), 1 (days), 2 (months), 3 (years). +2. A `period`, defining how often (in `time_unit`) it has to be paid. +3. An optional `recurrence_limit` of total payments to be paid. +4. An optional `recurrence_base`: + * `basetime`, defining when the first period starts + in seconds since 1970-01-01 UTC. + * `start_any_period` if non-zero, meaning you don't have to start + paying at the period indicated by `basetime`, but can use + `recurrence_start` to indicate what period you are starting at. +5. An optional `recurrence_paywindow`: + * `seconds_before`, defining how many seconds prior to the start of + the period a payment will be accepted. + * `proportional_amount`, if set indicating that a payment made during + the period itself will be charged proportionally to the remaining time + in the period (e.g. 150 seconds into a 1500 second period gives a 10% + discount). + * `seconds_after`, defining how many seconds after the start of the period + a payment will be accepted. + If this field is missing, payment will be accepted during the prior period and + the paid-for period. + +Note that the `absolute_expiry` field already covers the case where an offer +is no longer valid after January 1st 2021. + +## Offer Period Calculation + +Each period has a zero-based index, and a start time and an end time. +Because the periods can be in non-seconds units, the duration of a +period can depend on when it starts. The period with index N+1 begins +immediately following the end of period with index N. + +- if an offer contains `recurrence_base`: + - the start of period #0 is `basetime` seconds since 1970-01-01 UTC. +- otherwise: + - the start of period #0 is the time of issuance of the first + `invoice` for this particular offer and `payer_key`. + +To calculate the start of period #`N` for `N` > 0: +- if `time_unit` is 0: + - period `N` starts at period #0 start plus `period` multiplied by `N`, + in seconds. +- otherwise, if `time_unit` is 1: + - calculate the offset in seconds within the day of period #0 start. + - add `period` multiplied by `N` days to get the day of the period start. + - add the offset in seconds to get the period end in seconds. +- otherwise, if `time_unit` is 2: + - calculate the offset in days within the month of period #0 start. + - calculate the offset in seconds within the day of period #0 start. + - add `period` multiplied by `N` months to get the month of the period start. + - add the offset days to get the day of the period start. + - if the day is not within the month, use the last day within the month. + - add the offset seconds to get the period start in seconds. +- otherwise, if `time_unit` is 3: + - calculate the offset in months within the year of period #0 start. + - calculate the offset in days within the month of period #0 start. + - calculate the offset in seconds within the day of period #0 start. + - add `period` multiplied by `N` years to get the year of the period start. + - add the offset months to get the month of the period start. + - add the offset days to get the day of the period start. + - if the day is not within the month, use the last day within the month. + - add the offset seconds to get the period start in seconds. +- otherwise, the time is invalid. + +Note that offset seconds can overflow only if the period start is in a +leap second; we ignore this! + +See [offer-period-test.json](bolt12/offer-period-test.json). + +## Authorization + +Authorization is generally required for payments: without some +indication what someone intended to pay for and how much they intended +to pay, proof of payment is pointless. + +Normally this is simple: get the user to authorize the exact amount +and description before paying an invoice. With recurrence this +becomes more complex, as an implementation probably does not want to +prompt the user on every payment, but receive some initial +authorization to spend within a range (e.g. "Pay $5 AUD once a +week?"). In particular, the authorization may be in the user's native +currency, not the vendor's currency nor in Bitcoin. + +For example, consider an offer with weekly recurrence (`time_unit`=1, +`period`=7), `amount` 500, `currency` `AUD` ($5 Australian dollars). +An implementation may present this to the user as USD $3.53 (max +$3.71), to allow up to 5% exchange slippage, and receive their +authorization. As it received each invoice, it would convert the +`msat` into USD to check that it was below the maximum authorization +of USD$3.71. If it was, it would simply pay the invoice without user +interaction. + +On the other hand, if an invoice did exceed the authorization, it +would request reauthorization. It could also indicate whether it was +due to AUD/USD changes (since the offer indicated that was the +currency it was using) or a disagreement on the bitcoin exchange rate. + +Note that the problem is simpler for non-recurring offers, where +authorization may simply be delayed until the invoice is received and +the exact amount is known. + +Also, the implementation of a trusted exchange rate service is left to +the reader. + +## Requirements For Offers + +A writer of an offer: + - MUST set `node_id` to the public key of the node to request the invoice from. + - MUST specify exactly one signature TLV: `signature`. + - MUST set `sig` to the signature using `node_id` as described in [Signature Calculation](#signature-calculation). + - MUST set `description` to a complete description of the purpose + of the payment. + - if the chain for the invoice is not solely bitcoin: + - MUST specify `chains` the offer is valid for. + - otherwise: + - the bitcoin chain is implied as the first and only entry. + - if a specific minimum `amount` is required for successful payment: + - MUST specify `amount` to the amount expected (per item). + - if the currency for `amount` is that of the first entry in `chains`: + - MUST specify `amount` in multiples of the minimum lightning-payable unit + (e.g. milli-satoshis for bitcoin). + - otherwise: + - MUST specify `iso4217` as an ISO 4712 three-letter code. + - MUST specify `amount` in the currency unit adjusted by the ISO 4712 + exponent (e.g. USD cents). + - if it supports offer features: + - SHOULD set `features` to the bitmap of offer features. + - if the offer expires: + - MUST set `absolute_expiry` `seconds_from_epoch` to the number of seconds + after midnight 1 January 1970, UTC that invoice_request should not be + attempted. + - if it is connected only by private channels: + - MUST include `paths` containing one or more paths to the node from + publicly reachable nodes. + - otherwise: + - MAY include `paths`. + - if it includes `paths`: + - SHOULD ignore any invoice_request which does not use the path. + - if it sets `vendor`: + - SHOULD set it to clearly identify the issuer of the invoice. + - if it can supply more than one item for a single invoice + - if the minimum quantity is more than 1: + - MUST set that minimum in `quantity_min` + - if the maximum quantity is known: + - MUST set that maximum in `quantity_max` + - if neither: + - MUST set `quantity_min` to 1 to indicate `quantity` is supported. + - MAY include `recurrence` to indicate offer should trigger time-spaced + invoices. + - if it includes `recurrence`: + - MUST set `time_unit` to 0 (seconds), 1 (days), 2 (months), 3 (years). + - MUST set `period` to how often (per `time-unit`) it wants to be paid. + - if there is a maximum number of payments: + - MUST include `recurrence_limit` with `max_period` set to the maximum number of payments + - MUST NOT set `max_period` to 0. + - otherwise: + - MUST NOT include `recurrence_limit`. + - if periods are always at specific time offsets: + - MUST include `recurrence_base` + - MUST set `basetime` to the initial period time in number of + seconds after midnight 1 January 1970 + - if the first paid-for-period does not have to be the initial period: + - MUST set `start_any_period` to 1. + - otherwise: + - MUST set `start_any_period` to 0. + - otherwise: + - MUST NOT include `recurrence_base`. + - if payments will be accepted for the current or next period: + - MAY include `recurrence_paywindow` + - otherwise: + - MUST include `recurrence_paywindow` + - if it includes `recurrence_paywindow`: + - MUST set `seconds_before` to the maximum number of seconds prior to + a period for which it will accept payment or invoice_request for that period. + - MUST set `seconds_after` to the maximum number of seconds into to a + period for which it will accept payment or invoice_request for that period. + - MAY NOT enforce this for the initial period for offers without `recurrence_base` + - SHOULD NOT set `seconds_after` to greater than the maximum number of + seconds in a period. + - if it `amount` is specified and the node will proportionally reduce + the amount charged for a period payed after the start of the period: + - MUST set `proportional_amount` to 1 + - otherwise: + - MUST set `proportional_amount` to 0 + - otherwise: + - MUST NOT include `recurrence_base`. + - MUST NOT include `recurrence_paywindow`. + - MUST NOT include `recurrence_limit`. + - if `send_invoice` is present: + - if the offer is for a partial or full refund for a previously-paid + invoice: + - SHOULD set `refunded_payment_hash` to the `payment_hash` of that + invoice. + - otherwise: + - MUST NOT set `refunded_payment_hash`. + +A reader of an offer: + - if `features` contains unknown _odd_ bits that are non-zero: + - MUST ignore the bit. + - if `features` contains unknown _even_ bits that are non-zero: + - MUST NOT respond to the offer. + - SHOULD indicate the unknown bit to the user. + - if `node_id`, `description` or `signature` is not set: + - MUST NOT respond to the offer. + - if `description` is not present: + - MUST NOT respond to the offer. + - if `signature` is not a valid signature using `node_id` as described in [Signature Calculation](#signature-calculation): + - MUST NOT respond to the offer. + - SHOULD gain user consent for recurring payments. + - SHOULD allow user to view and cancel recurring payments. + - if it uses `amount` to provide the user with a cost estimate: + - MUST warn user if amount of actual invoice differs significantly + from that expectation. + - SHOULD not respond to an offer if the current time is after + `absolute_expiry`. + - FIXME: more! + +## Rationale + +It's quite reasonable to set a `recurrence_paywindow` with seconds_after +equal to 0, but obviously this should not apply to the initial period if +there is no recurrence_base. + +# Invoice Requests + +Invoice Requests are a request for an invoice; the human-readable prefix for +invoices is `lnr`. + +## TLV Fields for `invoice_request` + +1. tlvs: `invoice_request` +2. types: + 1. type: 2 (`chains`) + 2. data: + * [`...*chain_hash`:`chains`] + 1. type: 4 (`offer_id`) + 2. data: + * [`sha256`:`offer_id`] + 1. type: 8 (`amount`) + 2. data: + * [`tu64`:`msat`] + 1. type: 12 (`features`) + 2. data: + * [`...*byte`:`features`] + 1. type: 32 (`quantity`) + 2. data: + * [`tu64`:`quantity`] + 1. type: 36 (`recurrence_counter`) + 2. data: + * [`tu32`:`counter`] + 1. type: 68 (`recurrence_start`) + 2. data: + * [`tu32`:`period_offset`] + 1. type: 38 (`payer_key`) + 2. data: + * [`pubkey32`:`key`] + 1. type: 50 (`payer_info`) + 2. data: + * [`...*byte`:`blob`] + 1. type: 242 (`recurrence_signature`) + 2. data: + * [`bip340sig`:`sig`] + +## Requirements for Invoice Requests + +The writer of an invoice_request: + - MUST set `payer_key` to a transient public key. + - MUST remember the secret key corresponding to `payer_key`. + - MUST set `offer_id` to the merkle root of the offer as described in [Signature Calculation](#signature-calculation). + - MUST NOT set or imply any `chain_hash` not set or implied by the offer. + - if the offer had a `quantity_min` or `quantity_max` field: + - MUST set `quantity` + - MUST set it within that (inclusive) range. + - otherwise: + - MUST NOT set `quantity` + - if the offer did not specify `amount`: + - MUST specify `amount`.`msat` in multiples of the minimum lightning-payable unit + (e.g. milli-satoshis for bitcoin) for the first `chains` entry. + - otherwise: + - MAY omit `amount`. + - if it sets `amount`: + - MUST specify `amount`.`msat` as greater or equal to amount expected by the offer + (before any proportional period amount). + - if the offer contained `recurrence`: + - for the initial request: + - MUST use a unique `payer_key`. + - MUST set `recurrence_counter` `counter` to 0. + - for any successive requests: + - MUST use the same `payer_key` as the initial request. + - MUST set `recurrence_counter` `counter` to one greater than the highest-paid invoice. + - if the offer contained `recurrence_base` with `start_any_period` non-zero: + - MUST include `recurrence_start` + - MUST set `period_offset` to the period the sender wants for the initial request + - MUST set `period_offset` to the same value on all following requests. + - otherwise: + - MUST NOT include `recurrence_start` + - MAY set `payer_info` to arbitrary data to be reflected into the invoice. + - MUST set `recurrence_signature` `sig` as detailed in + [Signature Calculation](#signature-calculation) using the `payer_key`. + - if the offer contained `recurrence_limit`: + - MUST NOT send an `invoice_request` for a period greater than `max_period` + - SHOULD NOT send an `invoice_request` for a period which has + already passed. + - if the offer contains `recurrence_paywindow`: + - if the offer has a `recurrence_basetime` or the `recurrence_counter` is non-zero: + - SHOULD NOT send an `invoice_request` for a period prior to `seconds_before` seconds before that period start. + - SHOULD NOT send an `invoice_request` for a period later than `seconds_after` seconds past that period start. + - otherwise: + - SHOULD NOT send an `invoice_request` with `recurrence_counter` + is non-zero for a period whose immediate predecessor has not + yet begun. + - otherwise: + - MUST NOT set `recurrence_counter`. + - MUST NOT set `recurrence_signature`. + - MUST NOT set `recurrence_start` + +The reader of an invoice_request: + - MUST fail the request if `payer_key` is not present. + - MUST fail the request if `chains` does not include (or imply) a supported chain. + - MUST fail the request if `features` contains unknown even bits. + - MUST fail the request if `offer_id` is not present. + - MUST fail the request if the `offer_id` does not refer to an unexpired offer. + - if the offer had a `quantity_min` or `quantity_max` field: + - MUST fail the request if there is no `quantity` field. + - MUST fail the request if there is `quantity` is not within that (inclusive) range. + - otherwise: + - MUST fail the request if there is a `quantity` field. + - if the offer included `amount`: + - MUST calculate the *base invoice amount* using the offer `amount`: + - if offer `currency` is not the invoice currency, convert to the + invoice currency. + - if request contains `quantity`, multiply by `quantity`. + - if the request contains `amount`: + - MUST fail the request if its `amount` is less than the *base invoice amount*. + - MAY fail the request if its `amount` is much greater than the *base invoice amount*. + - MUST use the request's `amount` as the *base invoice amount*. + - otherwise: + - MUST fail the request if it does not contain `amount`. + - MUST use the request `amount` as the *base invoice amount*. (Note: invoice amount can be further modiifed by recurrence below) + - if the offer had a `recurrence`: + - MUST fail the request if there is no `recurrence_counter` field. + - MUST fail the request if there is no `recurrence_signature` field. + - MUST fail the request if `recurrence_signature` is not correct. + - if the offer had `recurrence_base` and `start_any_period` was 1: + - MUST fail the request if there is no `recurrence_start` field. + - MUST consider the period index for this request to be the + `recurrence_start` field plus the `recurrence_counter` `counter` + field. + - otherwise: + - MUST fail the request if there is a `recurrence_start` field. + - MUST consider the period index for this request to be the + `recurrence_counter` `counter` field. + - if the offer has a `recurrence_limit`: + - MUST fail the request if the period index is greater than `max_period`. + - MUST calculate the period using the period index as detailed in [Period Calculation](#offer-period-calculation). + - if `recurrence_counter` is non-zero: + - MUST fail the request if the no invoice for the previous period + has been paid. + - if the offer had a `recurrence_paywindow`: + - SHOULD fail the request if the current time is before the start of + the period minus `seconds_before`. + - SHOULD fail the request if the current time is equal to or after the + start of the period plus `seconds_after`. + - if `proportional_amount` is 1: + - MUST adjust the *base invoice amount* proportional to time remaining in + the period. + - otherwise: + - if `counter` is non-zero: + - SHOULD fail the request if the current time is prior to the start + of the previous period. + - otherwise (the offer had no `recurrence`): + - MUST fail the request if there is a `recurrence_counter` field. + - MUST fail the request if there is a `recurrence_signature` field. + +## Rationale + +We insist that recurring requests be in order (thus, if you pay an +invoice for #34 of a recurring offer, it implicitly commits to the +successful payment of #0 through #33). + +The `recurrence_paywindow` constrains how far you can pay in advance +precisely, and if it isn't in the offer the defaults provide some +slack, without allowing commitments into the far future. + +To avoid probing (should a payer_key become public in some way), we +require a signature for recurring invoice requests; this ensures that +no third party can determine how many invoices have been paid already. + +`payer_info` might typically contain information about the derivation of the +`payer_key`. This should not leak any information (such as using a simple +BIP-32 derivation path); a valid system might be for a node to maintain a base +payer key, and encode a 128-bit tweak here. The payer_key would be derived by +tweaking the base key with SHA256(payer_base_pubkey || tweak). + +Users can give a tip (or obscure the amount sent) by specifying an +`amount` in their invoice request, even though the offer specifies an +`amount`. Obviously this will only be accepted by the recipient if +the invoice request amount exceeds the amount it's expecting (i.e. its +`amount` after any currency conversion, multiplied by `quantity` if +any). Note that for recurring invoices with `proportional_amount` +set, the `amount` in the invoice request will be scaled by the time in +the period; the sender should not attempt to scale it. + +# Invoices + +Invoices are a request for payment, and when the payment is made they +it can be combined with the invoice to form a cryptographic receipt. + +The human-readable prefix for invoices is `lni`. It can be sent in +response to an `invoice_request` or an `offer` with `send_invoice` +using `onion_message` `invoice` field. + +1. tlvs: `invoice` +2. types: + 1. type: 2 (`chains`) + 2. data: + * [`...*chain_hash`:`chains`] + 1. type: 4 (`offer_id`) + 2. data: + * [`sha256`:`offer_id`] + 1. type: 8 (`amount`) + 2. data: + * [`tu64`:`msat`] + 1. type: 10 (`description`) + 2. data: + * [`...*utf8`:`description`] + 1. type: 12 (`features`) + 2. data: + * [`...*byte`:`features`] + 1. type: 16 (`paths`) + 2. data: + * [`...*blinded_path`:`paths`] + 1. type: 18 (`blindedpay`) + 2. data: + * [`...*blinded_payinfo`:`payinfo`] + 1. type: 20 (`vendor`) + 2. data: + * [`...*utf8`:`vendor`] + 1. type: 30 (`node_id`) + 2. data: + * [`pubkey32`:`node_id`] + 1. type: 32 (`quantity`) + 2. data: + * [`tu64`:`quantity`] + 1. type: 34 (`refund_for`) + 2. data: + * [`sha256`:`refunded_payment_hash`] + 1. type: 36 (`recurrence_counter`) + 2. data: + * [`tu32`:`counter`] + 1. type: 54 (`send_invoice`) + 1. type: 68 (`recurrence_start`) + 2. data: + * [`tu32`:`period_offset`] + 1. type: 64 (`recurrence_basetime`) + 2. data: + * [`tu64`:`basetime`] + 1. type: 38 (`payer_key`) + 2. data: + * [`pubkey32`:`key`] + 1. type: 50 (`payer_info`) + 2. data: + * [`...*byte`:`blob`] + 1. type: 40 (`timestamp`) + 2. data: + * [`tu64`:`timestamp`] + 1. type: 42 (`payment_hash`) + 2. data: + * [`sha256`:`payment_hash`] + 1. type: 44 (`relative_expiry`) + 2. data: + * [`tu32`:`seconds_from_timestamp`] + 1. type: 46 (`cltv`) + 2. data: + * [`tu32`:`min_final_cltv_expiry`] + 1. type: 48 (`fallbacks`) + 2. data: + * [`u8`:`num`] + * [`num*fallback_address`:`fallbacks`] + 1. type: 52 (`refund_signature`) + 2. data: + * [`bip340sig`:`payer_signature`] + 1. type: 240 (`signature`) + 2. data: + * [`bip340sig`:`sig`] + +1. subtype: `blinded_payinfo` +2. data: + * [`u32`:`fee_base_msat`] + * [`u32`:`fee_proportional_millionths`] + * [`u16`:`cltv_expiry_delta`] + * [`u16`:`flen`] + * [`flen*byte`:`features`] + +1. subtype: `fallback_address` +2. data: + * [`byte`:`version`] + * [`u16`:`len`] + * [`len*byte`:`address`] + +## Requirements + +A writer of an invoice: + - MUST set `timestamp` to the number of seconds since Midnight 1 + January 1970, UTC. + - MUST set `payment_hash` to the SHA2 256-bit hash of the + `payment_preimage` that will be given in return for payment. + - MUST set (or not set) `send_invoice` the same as the offer. + - MUST set `offer_id` to the id of the offer. + - MUST specify exactly one signature TLV: `signature`. + - MUST set `sig` to the signature using `node_id` as described in [Signature Calculation](#signature-calculation). + - if the chain for the invoice is not solely bitcoin: + - MUST specify `chains` the invoice is valid for. + - otherwise: + - the bitcoin chain is implied as the first and only entry. + - if it has bolt11 features: + - MUST set `features` to the bitmap of features. + - if the invoice corresponds to an offer with `recurrence`: + - MUST set `recurrence_basetime` to the start of period #0 as calculated + by [Period Calculation](#offer-period-calculation). + - if it sets `relative_expiry`: + - MUST NOT set `relative_expiry` `seconds_from_timestamp` more than the number of seconds after `timestamp` that payment for this period will be accepted. + - otherwise: + - MUST not set `recurrence_basetime`. + - if the expiry for accepting payment is not 7200 seconds after `timestamp`: + - MUST set `relative_expiry` `seconds_from_timestamp` to the number of + seconds after `timestamp` that payment of this invoice should not be attempted. + - if the `min_final_cltv_expiry` for the last HTLC in the route is not 18: + - MUST set `min_final_cltv_expiry`. + - if it accepts onchain payments: + - MAY specify `fallbacks` + - MUST specify `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 + program + - if it is connected only by private channels: + - MUST include a `blinded_path` containing one or more paths to the node. + - otherwise: + - MAY include `blinded_path`. + - if it includes `blinded_path`: + - MUST specify `path` in order of most-preferred to least-preferred if + it has a preference. + - MUST include `blinded_payinfo` with exactly one `payinfo` for + each `onionmsg_path` in `blinded_path`, in order. + - SHOULD ignore any payment which does not use one of the paths. + - otherwise: + - MUST NOT include `blinded_payinfo`. + - MUST set `vendor` exactly as the offer did. + - MUST specify `amount`.`msat` in multiples of the minimum lightning-payable unit + (e.g. milli-satoshis for bitcoin) for the first `chains` entry. + - if responding to an `invoice_request`: + - if for the same `offer_id`, `payer_key` and `recurrence_counter` (if any) as a previous `invoice_request`: + - MAY simply reuse the previous invoice. + - otherwise: + - MUST NOT reuse a previous invoice. + - MUST set `node_id` the same as the offer. + - MUST set (or not set) `quantity` exactly as the invoice_request did. + - MUST set (or not set) `recurrence_counter` exactly as the invoice_request did. + - MUST set (or not set) `recurrence_start` exactly as the invoice_request did. + - MUST set `payer_key` exactly as the invoice_request did. + - MUST set (or not set) `payer_info` exactly as the invoice_request did. + - MUST begin `description` with the `description` from the offer. + - MAY append additional information to `description` (e.g. " +shipping"). + - if it does not set `amount` to the *base invoice amount* calculated from the invoice_request: + - MUST append the reason to `description` (e.g. " 5% bulk discount"). + - MUST NOT set `refund_for` + - MUST NOT set `refund_signature` + - otherwise (responding to a `send_invoice` offer): + - MUST set `node_id` to the id of the node to send payment to. + - MUST set `description` the same as the offer. + - if the offer had a `quantity_min` or `quantity_max` field: + - MUST set `quantity` + - MUST set it within that (inclusive) range. + - otherwise: + - MUST NOT set `quantity` + - MUST set `payer_key` to the `node_id` of the offer. + - MUST NOT set `payer_info`. + - MUST set (or not set) `refund_for` exactly as the offer did. + - if it sets `refund_for`: + - MUST set `refund_signature` to the signature of the + `refunded_payment_hash` using prefix `refund_signature` and the `payer_key` from the to-be-refunded invoice. + - otherwise: + - MUST NOT set `refund_signature` + - FIXME: recurrence! + +A reader of an invoice: + - MUST reject the invoice if `signature` is not a valid signature using `node_id` as described in [Signature Calculation](#signature-calculation). + - MUST reject the invoice if `msat` is not present. + - MUST reject the invoice if `description` is not present. + - MUST reject the invoice if `timestamp` is not present. + - MUST reject the invoice if `payment_hash` is not present. + - if `relative_expiry` is present: + - MUST reject the invoice if the current time since 1970-01-01 UTC is greater than `timestamp` plus `seconds_from_timestamp`. + - otherwise: + - MUST reject the invoice if the current time since 1970-01-01 UTC is greater than `timestamp` plus 7200. + - if `blinded_path` is present: + - MUST reject the invoice if `blinded_payinfo` is not present. + - MUST reject the invoice if `blinded_payinfo` does not contain exactly as many `payinfo` as total `onionmsg_path` in `blinded_path`. + - SHOULD confirm authorization if `msat` is not within the amount range authorized. + - if the invoice is a reply to an `invoice_request`: + - MUST reject the invoice unless `offer_id` is equal to the id of the offer. + - MUST reject the invoice unless `node_id` is equal to the offer. + - MUST reject the invoice unless the following fields are equal or unset + exactly as they are in the `invoice_request:` + - `quantity` + - `recurrence_counter` + - `recurrence_start` + - `payer_key` + - `payer_info` + - SHOULD confirm authorization if the `description` does not exactly + match the `offer` + - MAY highlight if `description` has simply had a change appended. + - SHOULD confirm authorization if `vendor` does not exactly + match the `offer`. + - otherwise if `offer_id` is set: + - MUST reject the invoice if the `offer_id` does not refer an unexpired offer with `send_invoice` + - MUST reject the invoice unless the following fields are equal or unset + exactly as they are in the `offer`: + - `refund_for` + - `description` + - `vendor` + - if the offer had a `quantity_min` or `quantity_max` field: + - MUST reject the invoice if there is no `quantity` field. + - MUST reject the invoice if there is `quantity` is not within that (inclusive) range. + - otherwise: + - MUST reject the invoice if there is a `quantity` field. + - if the offer contained `recurrence`: + - MUST reject the invoice if `recurrence_basetime` is not set. + - if the offer contained `refund_for`: + - MUST reject the invoice if `payer_key` does not match the invoice whose `payment_hash` is equal to `refund_for` `refunded_payment_hash` + - MUST reject the invoice if `refund_signature` is not set. + - MUST reject the invoice if `refund_signature` is not a valid signature using `payer_key` as described in [Signature Calculation](#signature-calculation). + - for the bitcoin chain, if the invoice specifies `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` + +## Rationale + +Because the messaging layer is unreliable, it's quite possible to +receive multiple requests for the same offer. As it's the caller's +responsibility not to reuse `payer_key` except for recurring invoices, +the writer doesn't have to check all the fields are duplicates before +simply returning a previous invoice. + +The invoice duplicates fields rather than committing to the previous offer or +invoice_request. This flattened format simplifies storage at some space cost, as +the payer need only remember the invoice for any refunds or proof. + +The recurrence_basetime similarly enables calculation of the next period +without having to refer to the initial invoice (in the case where the +offer does not contain `recurrence_base`. + +The reader of the invoice cannot trust the invoice correctly reflects the +offer and invoice_request fields, hence the requirements to check that they +are correct. + +Note that the recipient of the invoice can determine the expected +amount from either the offer it received, or the invoice_request it +sent, so often already has authorization for the expected amount. + +It's natural to set the `relative_expiry` of an invoice for a +recurring offer to the end of the payment window for the period, but +if that is a long time and the offer was in another currency, it's +common to cap this at some maximum duration. For example, omitting it +implies the default of 7200 seconds, which is generally a sufficient +time for payment. + +# Invoice Errors + +Informative errors can be returned in an onion message `invoice_error` +field (via the onion `reply_path`) for either `invoice_request` or +`invoice`. + +## TLV Fields for `invoice_error` + +1. tlvs: `invoice_error` +2. types: + 1. type: 1 (`erroneous_field`) + 2. data: + * [`tu64`:`tlv_fieldnum`] + 1. type: 3 (`suggested_value`) + 2. data: + * [`...*byte`:`value`] + 1. type: 5 (`error`) + 2. data: + * [`...*utf8`:`msg`] + +## Requirements + +A writer of an invoice_error: + - MUST set `error` to an explanatory string. + - MAY set `erroneous_field` to a specific field number in the + `invoice` or `invoice_request` which had a problem. + - if it sets `erroneous_field`: + - MAY set `suggested_value`. + - if it sets `suggested_value`: + - MUST set `suggested_value` to a valid field for that `tlv_fieldnum`. + - otherwise: + - MUST NOT set `suggested_value`. + +A reader of an invoice_error: + FIXME! + +## Rationale + +Usually an error message is sufficient for diagnostics, however there +is at least one case where it should be programatically parsable. A +recurring offer which sets `send_invoice` can also specify a currency, +which opens the possibility for a disagreement on exchange rate. In +this case, the `suggested_value` reflects its expected value, and the +sender can send a new invoice. + +# FIXME: Possible future extensions: + +1. The offer can require delivery info in the invreq. +2. An offer can be updated: the response to an invreq is another offer, + perhaps with a signature from the original node_id +3. Any empty TLV fields can mean the value is supposed to be known by + other means (i.e. transport-specific), but is still hashed for sig. +4. We could upgrade to allow multiple offers in one invoice_request and + invoice, to make a shopping list. +7. All-zero offer_id == gratuitous payment. +8. Recurrent invoice requests? + +[1] https://www.youtube.com/watch?v=4SYc_flMnMQ diff --git a/bolt12/format-string-test.json b/bolt12/format-string-test.json new file mode 100644 index 000000000..a273cbbaf --- /dev/null +++ b/bolt12/format-string-test.json @@ -0,0 +1,47 @@ +[ + { + "comment": "A complete string is valid", + "valid": true, + "string": "lni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrfl7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqqqqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7uerp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw09wtz97jt55m3cvdzsktqwu" + }, + { + "comment": "+ can join anywhere", + "valid": true, + "string": "l+ni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrfl7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqqqqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7uerp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw09wtz97jt55m3cvdzsktqwu" + }, + { + "comment": "Multiple + can join", + "valid": true, + "string": "lni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrf+l7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqq+qqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7u+erp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq+67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw+4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw0+9wtz97jt55m3cvdzsktqwu" + }, + { + "comment": "+ can be followed by whitespace", + "valid": true, + "string": "lni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrf+ l7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqq+ qqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7u+\terp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq+\n67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw+\r\n4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw0+\r 9wtz97jt55m3cvdzsktqwu" + }, + { + "comment": "+ must be surrounded by bech32 characters", + "valid": false, + "string": "lni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrfl7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqqqqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7uerp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw09wtz97jt55m3cvdzsktqwu+" + }, + { + "comment": "+ must be surrounded by bech32 characters", + "valid": false, + "string": "lni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrfl7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqqqqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7uerp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw09wtz97jt55m3cvdzsktqwu+ " + }, + { + "comment": "+ must be surrounded by bech32 characters", + "valid": false, + "string": "+lni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrfl7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqqqqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7uerp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw09wtz97jt55m3cvdzsktqwu" + }, + { + "comment": "+ must be surrounded by bech32 characters", + "valid": false, + "string": "+ lni1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrfl7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqqqqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7uerp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw09wtz97jt55m3cvdzsktqwu" + }, + { + "comment": "+ must be surrounded by bech32 characters", + "valid": false, + "string": "ln++i1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcyyprh7m68pmdr6rr0zqrfl7gtfu3jthqqrzpqjmu8vnmwny9swty4xzqpqy9quun9vd6hyunfdenjqar9wd6qcrvqqqqqqqqqqqqqqqqzsgqpuggz953rvg9rtxj8lalh43z8epwydjfrmffn3y3p5qz5cywpu09rr4vjgqgpycss9zvdnt7uerp970wvapvfnd7dut079yf0mr5tkzpu4068lv23fd6y9qz9lg08ng4zpczkdfjzr02pxp55c2htgzuq67hrtr7qpw9hesgraze7e29d2cvu9sqhwtspq5epqj8efghvyq5cvvhd7350vt9t4g2qq306reu67pqw4tj46tcr3fxqyqwav4xmu5mkesvyhdujf28wyrm5g9jwf5nehtsge8zlp5wnndzh5z5j7g44jfzu0dw09wtz97jt55m3cvdzsktqwu" + }, +] diff --git a/bolt12/offer-period-test.json b/bolt12/offer-period-test.json new file mode 100644 index 000000000..c923c7f3b --- /dev/null +++ b/bolt12/offer-period-test.json @@ -0,0 +1,527 @@ +[ + { + "comment": "If a period starts at 10:00:00 (am) UTC on 31 December, 1970", + "basetime": 31485600, + "time_unit": 0, + "period": 1, + "n": 1, + "expect": { + "timestring": "31 Dec 1970 10:00:01 UTC", + "seconds_since_epoch": 31485601 + } + }, + { + "basetime": 31485600, + "time_unit": 0, + "period": 10, + "n": 1, + "expect": { + "timestring": "31 Dec 1970 10:00:10 UTC", + "seconds_since_epoch": 31485610 + } + }, + { + "basetime": 31485600, + "time_unit": 0, + "period": 100, + "n": 1, + "expect": { + "timestring": "31 Dec 1970 10:01:40 UTC", + "seconds_since_epoch": 31485700 + } + }, + { + "basetime": 31485600, + "time_unit": 0, + "period": 1000, + "n": 1, + "expect": { + "timestring": "31 Dec 1970 10:16:40 UTC", + "seconds_since_epoch": 31486600 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "period": 1, + "n": 1, + "expect": { + "timestring": "1 Jan 1971 10:00:00 UTC", + "seconds_since_epoch": 31572000 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "period": 10, + "n": 1, + "expect": { + "timestring": "10 Jan 1971 10:00:00 UTC", + "seconds_since_epoch": 32349600 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "period": 100, + "n": 1, + "expect": { + "timestring": "10 Apr 1971 10:00:00 UTC", + "seconds_since_epoch": 40125600 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "period": 1000, + "n": 1, + "expect": { + "timestring": "26 Sep 1973 10:00:00 UTC", + "seconds_since_epoch": 117885600 + } + }, + { + "basetime": 31485600, + "time_unit": 2, + "period": 1, + "n": 1, + "expect": { + "timestring": "31 Jan 1971 10:00:00 UTC", + "seconds_since_epoch": 34164000 + } + }, + { + "basetime": 31485600, + "time_unit": 2, + "period": 10, + "n": 1, + "expect": { + "timestring": "31 Oct 1971 10:00:00 UTC", + "seconds_since_epoch": 57751200 + } + }, + { + "basetime": 31485600, + "time_unit": 2, + "period": 100, + "n": 1, + "expect": { + "timestring": "30 Apr 1979 10:00:00 UTC", + "seconds_since_epoch": 294314400 + } + }, + { + "basetime": 31485600, + "time_unit": 3, + "period": 1, + "n": 1, + "expect": { + "timestring": "31 Dec 1971 10:00:00 UTC", + "seconds_since_epoch": 63021600 + } + }, + { + "basetime": 31485600, + "time_unit": 3, + "period": 10, + "n": 1, + "expect": { + "timestring": "31 Dec 1980 10:00:00 UTC", + "seconds_since_epoch": 347104800 + } + }, + { + "comment": "If a period starts at 10:00:00 (am) UTC on 29 February, 2016", + "basetime": 1456740000, + "time_unit": 0, + "period": 1, + "n": 1, + "expect": { + "timestring": "29 Feb 2016 10:00:01 UTC", + "seconds_since_epoch": 1456740001 + } + }, + { + "basetime": 1456740000, + "time_unit": 0, + "period": 10, + "n": 1, + "expect": { + "timestring": "29 Feb 2016 10:00:10 UTC", + "seconds_since_epoch": 1456740010 + } + }, + { + "basetime": 1456740000, + "time_unit": 0, + "period": 100, + "n": 1, + "expect": { + "timestring": "29 Feb 2016 10:01:40 UTC", + "seconds_since_epoch": 1456740100 + } + }, + { + "basetime": 1456740000, + "time_unit": 0, + "period": 1000, + "n": 1, + "expect": { + "timestring": "29 Feb 2016 10:16:40 UTC", + "seconds_since_epoch": 1456741000 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "period": 1, + "n": 1, + "expect": { + "timestring": "1 Mar 2016 10:00:00 UTC", + "seconds_since_epoch": 1456826400 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "period": 10, + "n": 1, + "expect": { + "timestring": "10 Mar 2016 10:00:00 UTC", + "seconds_since_epoch": 1457604000 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "period": 100, + "n": 1, + "expect": { + "timestring": "08 Jun 2016 10:00:00 UTC", + "seconds_since_epoch": 1465380000 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "period": 1000, + "n": 1, + "expect": { + "timestring": "25 Nov 2018 10:00:00 UTC", + "seconds_since_epoch": 1543140000 + } + }, + { + "basetime": 1456740000, + "time_unit": 2, + "period": 1, + "n": 1, + "expect": { + "timestring": "29 Mar 2016 10:00:00 UTC", + "seconds_since_epoch": 1459245600 + } + }, + { + "basetime": 1456740000, + "time_unit": 2, + "period": 10, + "n": 1, + "expect": { + "timestring": "29 Dec 2016 10:00:00 UTC", + "seconds_since_epoch": 1483005600 + } + }, + { + "basetime": 1456740000, + "time_unit": 2, + "period": 100, + "n": 1, + "expect": { + "timestring": "29 Jun 2024 10:00:00 UTC", + "seconds_since_epoch": 1719655200 + } + }, + { + "basetime": 1456740000, + "time_unit": 3, + "period": 1, + "n": 1, + "expect": { + "timestring": "28 Feb 2017 10:00:00 UTC", + "seconds_since_epoch": 1488276000 + } + }, + { + "basetime": 1456740000, + "time_unit": 3, + "period": 10, + "n": 1, + "expect": { + "timestring": "28 Feb 2026 10:00:00 UTC", + "seconds_since_epoch": 1772272800 + } + }, + + { + "comment": "period and n are actually substitutable!", + "basetime": 31485600, + "time_unit": 0, + "n": 1, + "period": 1, + "expect": { + "timestring": "31 Dec 1970 10:00:01 UTC", + "seconds_since_epoch": 31485601 + } + }, + { + "basetime": 31485600, + "time_unit": 0, + "n": 10, + "period": 1, + "expect": { + "timestring": "31 Dec 1970 10:00:10 UTC", + "seconds_since_epoch": 31485610 + } + }, + { + "basetime": 31485600, + "time_unit": 0, + "n": 100, + "period": 1, + "expect": { + "timestring": "31 Dec 1970 10:01:40 UTC", + "seconds_since_epoch": 31485700 + } + }, + { + "basetime": 31485600, + "time_unit": 0, + "n": 1000, + "period": 1, + "expect": { + "timestring": "31 Dec 1970 10:16:40 UTC", + "seconds_since_epoch": 31486600 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "n": 1, + "period": 1, + "expect": { + "timestring": "1 Jan 1971 10:00:00 UTC", + "seconds_since_epoch": 31572000 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "n": 10, + "period": 1, + "expect": { + "timestring": "10 Jan 1971 10:00:00 UTC", + "seconds_since_epoch": 32349600 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "n": 100, + "period": 1, + "expect": { + "timestring": "10 Apr 1971 10:00:00 UTC", + "seconds_since_epoch": 40125600 + } + }, + { + "basetime": 31485600, + "time_unit": 1, + "n": 1000, + "period": 1, + "expect": { + "timestring": "26 Sep 1973 10:00:00 UTC", + "seconds_since_epoch": 117885600 + } + }, + { + "basetime": 31485600, + "time_unit": 2, + "n": 1, + "period": 1, + "expect": { + "timestring": "31 Jan 1971 10:00:00 UTC", + "seconds_since_epoch": 34164000 + } + }, + { + "basetime": 31485600, + "time_unit": 2, + "n": 10, + "period": 1, + "expect": { + "timestring": "31 Oct 1971 10:00:00 UTC", + "seconds_since_epoch": 57751200 + } + }, + { + "basetime": 31485600, + "time_unit": 2, + "n": 100, + "period": 1, + "expect": { + "timestring": "30 Apr 1979 10:00:00 UTC", + "seconds_since_epoch": 294314400 + } + }, + { + "basetime": 31485600, + "time_unit": 3, + "n": 1, + "period": 1, + "expect": { + "timestring": "31 Dec 1971 10:00:00 UTC", + "seconds_since_epoch": 63021600 + } + }, + { + "basetime": 31485600, + "time_unit": 3, + "n": 10, + "period": 1, + "expect": { + "timestring": "31 Dec 1980 10:00:00 UTC", + "seconds_since_epoch": 347104800 + } + }, + { + "comment": "If a period starts at 10:00:00 (am) UTC on 29 February, 2016", + "basetime": 1456740000, + "time_unit": 0, + "n": 1, + "period": 1, + "expect": { + "timestring": "29 Feb 2016 10:00:01 UTC", + "seconds_since_epoch": 1456740001 + } + }, + { + "basetime": 1456740000, + "time_unit": 0, + "n": 10, + "period": 1, + "expect": { + "timestring": "29 Feb 2016 10:00:10 UTC", + "seconds_since_epoch": 1456740010 + } + }, + { + "basetime": 1456740000, + "time_unit": 0, + "n": 100, + "period": 1, + "expect": { + "timestring": "29 Feb 2016 10:01:40 UTC", + "seconds_since_epoch": 1456740100 + } + }, + { + "basetime": 1456740000, + "time_unit": 0, + "n": 1000, + "period": 1, + "expect": { + "timestring": "29 Feb 2016 10:16:40 UTC", + "seconds_since_epoch": 1456741000 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "n": 1, + "period": 1, + "expect": { + "timestring": "1 Mar 2016 10:00:00 UTC", + "seconds_since_epoch": 1456826400 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "n": 10, + "period": 1, + "expect": { + "timestring": "10 Mar 2016 10:00:00 UTC", + "seconds_since_epoch": 1457604000 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "n": 100, + "period": 1, + "expect": { + "timestring": "08 Jun 2016 10:00:00 UTC", + "seconds_since_epoch": 1465380000 + } + }, + { + "basetime": 1456740000, + "time_unit": 1, + "n": 1000, + "period": 1, + "expect": { + "timestring": "25 Nov 2018 10:00:00 UTC", + "seconds_since_epoch": 1543140000 + } + }, + { + "basetime": 1456740000, + "time_unit": 2, + "n": 1, + "period": 1, + "expect": { + "timestring": "29 Mar 2016 10:00:00 UTC", + "seconds_since_epoch": 1459245600 + } + }, + { + "basetime": 1456740000, + "time_unit": 2, + "n": 10, + "period": 1, + "expect": { + "timestring": "29 Dec 2016 10:00:00 UTC", + "seconds_since_epoch": 1483005600 + } + }, + { + "basetime": 1456740000, + "time_unit": 2, + "n": 100, + "period": 1, + "expect": { + "timestring": "29 Jun 2024 10:00:00 UTC", + "seconds_since_epoch": 1719655200 + } + }, + { + "basetime": 1456740000, + "time_unit": 3, + "n": 1, + "period": 1, + "expect": { + "timestring": "28 Feb 2017 10:00:00 UTC", + "seconds_since_epoch": 1488276000 + } + }, + { + "basetime": 1456740000, + "time_unit": 3, + "n": 10, + "period": 1, + "expect": { + "timestring": "28 Feb 2026 10:00:00 UTC", + "seconds_since_epoch": 1772272800 + } + } +] From 41b56807c1a1a6810e5184eed090a9822dcf22d2 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 16 Apr 2021 15:19:02 +0930 Subject: [PATCH 7/9] fixup! BOLT 12: offers, third draft Make spellchecker happy. --- .aspell.en.pws | 9 +++++++++ 12-offer-encoding.md | 32 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.aspell.en.pws b/.aspell.en.pws index d247edfa7..6c4ddd1d7 100644 --- a/.aspell.en.pws +++ b/.aspell.en.pws @@ -387,3 +387,12 @@ CHECKSIGVERIFY IFDUP sats onionmsg +Schnorr +unblinded +Merkle +whitespace +rata +programatically +parsable +TLVs +AUD diff --git a/12-offer-encoding.md b/12-offer-encoding.md index 0cf403935..73d46794d 100644 --- a/12-offer-encoding.md +++ b/12-offer-encoding.md @@ -20,9 +20,9 @@ limitations: in other forms (i.e. inside the lightning network itself). 2. The signature applying to the entire invoice makes it impossible to prove an invoice without revealing its entirety. -3. Fields are not generally extractable for external use: the `h` +3. Fields cannot generally be extracted for external use: the `h` field was a boutique extraction of the `d` field, only. -4. The lack of 'it's ok to be odd' rule makes backwards compatibility +4. The lack of 'it's OK to be odd' rule makes backwards compatibility harder. 5. The 'human-readable' idea of separating amounts proved fraught: `p` was often mishandled, and amounts in pico-bitcoin are harder @@ -52,7 +52,7 @@ The general user-pays-merchant flow is: 4. The user makes a payment to the merchant indicated by the invoice. The merchant-pays-user flow (e.g. ATM or refund): -1. The merchant provides a user-specific *offer* ("take my money") in a webpage or QR code, +1. The merchant provides a user-specific *offer* ("take my money") in a web page or QR code, with an amount (for a refund, also a reference to the to-be-refunded invoice). 2. A user sends an *invoice* for the amount in the *offer* (for a @@ -69,11 +69,11 @@ claim they paid the invoice, too.[1] 1. Sharing the minimum information required to prove sections of the invoice in dispute (e.g. the description of the item, the payment hash, and the merchant's signature). -2. Contain a transferrable proof that the user is the one who was +2. Contain a transferable proof that the user is the one who was responsible for paying the invoice in the first place. Providing a key in *invoice_request* allows a user to prove that they were the one -to request the invoice. In addition, the merkle construction of the BOLT 12 +to request the invoice. In addition, the Merkle construction of the BOLT 12 invoice signature allows the user to selectively reveal fields of the invoice in case of dispute. @@ -123,16 +123,16 @@ here is `SIG(tag,msg,key)`. Each form is signed using one or more TLV signature elements; TLV types 240 through 1000 are considered signature elements. For these the tag is `lightning` | `messagename` | `fieldname`, and `msg` is the -merkle-root; `lightning` is the literal 9-byte ASCII string, +Merkle-root; `lightning` is the literal 9-byte ASCII string, `messagename` is the name of the TLV stream being signed (i.e. `offer` or `invoice`) and the `fieldname` is the TLV field containing the signature (e.g. `signature` or `recurrence_signature`). -The formulation of the merkle tree is similar to that proposed in +The formulation of the Merkle tree is similar to that proposed in [BIP-taproot], with the insertion of alternate "dummy" leaves to avoid revealing adjacent nodes in proofs. -The Merkle Tree's leaves are, in TLV-ascending order: +The Merkle tree's leaves are, in TLV-ascending order: 1. The SHA256 of: `LnLeaf` followed by the TLV entry. 2. The SHA256 of: `LnAll` followed all non-signature TLV entries appended in ascending order. @@ -177,7 +177,7 @@ Signature = SIG('lightningoffersignature', Root, nodekey) ## Rationale -FIXME: some taproot, some about obscuring leaves in merkle proofs. +FIXME: some taproot, some about obscuring leaves in Merkle proofs. # Offers @@ -364,7 +364,7 @@ of USD$3.71. If it was, it would simply pay the invoice without user interaction. On the other hand, if an invoice did exceed the authorization, it -would request reauthorization. It could also indicate whether it was +would request re-authorization. It could also indicate whether it was due to AUD/USD changes (since the offer indicated that was the currency it was using) or a disagreement on the bitcoin exchange rate. @@ -539,7 +539,7 @@ invoices is `lnr`. The writer of an invoice_request: - MUST set `payer_key` to a transient public key. - MUST remember the secret key corresponding to `payer_key`. - - MUST set `offer_id` to the merkle root of the offer as described in [Signature Calculation](#signature-calculation). + - MUST set `offer_id` to the Merkle root of the offer as described in [Signature Calculation](#signature-calculation). - MUST NOT set or imply any `chain_hash` not set or implied by the offer. - if the offer had a `quantity_min` or `quantity_max` field: - MUST set `quantity` @@ -609,7 +609,7 @@ The reader of an invoice_request: - MUST use the request's `amount` as the *base invoice amount*. - otherwise: - MUST fail the request if it does not contain `amount`. - - MUST use the request `amount` as the *base invoice amount*. (Note: invoice amount can be further modiifed by recurrence below) + - MUST use the request `amount` as the *base invoice amount*. (Note: invoice amount can be further modified by recurrence below) - if the offer had a `recurrence`: - MUST fail the request if there is no `recurrence_counter` field. - MUST fail the request if there is no `recurrence_signature` field. @@ -921,7 +921,7 @@ The invoice duplicates fields rather than committing to the previous offer or invoice_request. This flattened format simplifies storage at some space cost, as the payer need only remember the invoice for any refunds or proof. -The recurrence_basetime similarly enables calculation of the next period +The `recurrence_basetime` similarly enables calculation of the next period without having to refer to the initial invoice (in the case where the offer does not contain `recurrence_base`. @@ -987,9 +987,9 @@ sender can send a new invoice. # FIXME: Possible future extensions: -1. The offer can require delivery info in the invreq. -2. An offer can be updated: the response to an invreq is another offer, - perhaps with a signature from the original node_id +1. The offer can require delivery info in the `invoice_request`. +2. An offer can be updated: the response to an `invoice_request` is another offer, + perhaps with a signature from the original `node_id` 3. Any empty TLV fields can mean the value is supposed to be known by other means (i.e. transport-specific), but is still hashed for sig. 4. We could upgrade to allow multiple offers in one invoice_request and From fc8aab72ccdd616301dc200fc124824efe4fbb58 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 16 Apr 2021 15:21:57 +0930 Subject: [PATCH 8/9] BOLT 12: allow replacement of previous invoices, make signature on invoice_request compulsory We can ask the vendor to discard the previous invoice in the case of stuck payments: this discarded invoice gets mirrored into the new invoice, so if they actually don't do it and take both payments we can prove it. We need a signature using payer_key here, otherwise someone else could cancel our invoice. We previously only needed that signature for recurrence; now we rename it and make it always present for simplicity. Signed-off-by: Rusty Russell --- 12-offer-encoding.md | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/12-offer-encoding.md b/12-offer-encoding.md index 73d46794d..7ebd71bd1 100644 --- a/12-offer-encoding.md +++ b/12-offer-encoding.md @@ -530,7 +530,10 @@ invoices is `lnr`. 1. type: 50 (`payer_info`) 2. data: * [`...*byte`:`blob`] - 1. type: 242 (`recurrence_signature`) + 1. type: 56 (`replace_invoice`) + 2. data: + * [`sha256`:`payment_hash`] + 1. type: 242 (`payer_signature`) 2. data: * [`bip340sig`:`sig`] @@ -541,6 +544,7 @@ The writer of an invoice_request: - MUST remember the secret key corresponding to `payer_key`. - MUST set `offer_id` to the Merkle root of the offer as described in [Signature Calculation](#signature-calculation). - MUST NOT set or imply any `chain_hash` not set or implied by the offer. + - MUST set `payer_signature` `sig` as detailed in [Signature Calculation](#signature-calculation) using the `payer_key`. - if the offer had a `quantity_min` or `quantity_max` field: - MUST set `quantity` - MUST set it within that (inclusive) range. @@ -554,6 +558,9 @@ The writer of an invoice_request: - if it sets `amount`: - MUST specify `amount`.`msat` as greater or equal to amount expected by the offer (before any proportional period amount). + - if the sender has a previous unpaid invoice (for the same offer) which it wants to cancel: + - MUST set `payer_key` to the same as the previous invoice. + - MUST set `replace_invoice` to the `payment_hash` or the previous invoice. - if the offer contained `recurrence`: - for the initial request: - MUST use a unique `payer_key`. @@ -568,8 +575,6 @@ The writer of an invoice_request: - otherwise: - MUST NOT include `recurrence_start` - MAY set `payer_info` to arbitrary data to be reflected into the invoice. - - MUST set `recurrence_signature` `sig` as detailed in - [Signature Calculation](#signature-calculation) using the `payer_key`. - if the offer contained `recurrence_limit`: - MUST NOT send an `invoice_request` for a period greater than `max_period` - SHOULD NOT send an `invoice_request` for a period which has @@ -584,7 +589,6 @@ The writer of an invoice_request: yet begun. - otherwise: - MUST NOT set `recurrence_counter`. - - MUST NOT set `recurrence_signature`. - MUST NOT set `recurrence_start` The reader of an invoice_request: @@ -593,6 +597,8 @@ The reader of an invoice_request: - MUST fail the request if `features` contains unknown even bits. - MUST fail the request if `offer_id` is not present. - MUST fail the request if the `offer_id` does not refer to an unexpired offer. + - MUST fail the request if there is no `payer_signature` field. + - MUST fail the request if `payer_signature` is not correct. - if the offer had a `quantity_min` or `quantity_max` field: - MUST fail the request if there is no `quantity` field. - MUST fail the request if there is `quantity` is not within that (inclusive) range. @@ -610,10 +616,13 @@ The reader of an invoice_request: - otherwise: - MUST fail the request if it does not contain `amount`. - MUST use the request `amount` as the *base invoice amount*. (Note: invoice amount can be further modified by recurrence below) + - if the offer has a `replace_invoice`: + - if the `payment_hash` refers to an unpaid invoice for the same `offer_id` and `payer_key`: + - MUST immediately expire/remove that unpaid invoice such that it cannot be paid in future. + - otherwise: + - MUST fail the request. - if the offer had a `recurrence`: - MUST fail the request if there is no `recurrence_counter` field. - - MUST fail the request if there is no `recurrence_signature` field. - - MUST fail the request if `recurrence_signature` is not correct. - if the offer had `recurrence_base` and `start_any_period` was 1: - MUST fail the request if there is no `recurrence_start` field. - MUST consider the period index for this request to be the @@ -643,7 +652,6 @@ The reader of an invoice_request: of the previous period. - otherwise (the offer had no `recurrence`): - MUST fail the request if there is a `recurrence_counter` field. - - MUST fail the request if there is a `recurrence_signature` field. ## Rationale @@ -656,8 +664,9 @@ precisely, and if it isn't in the offer the defaults provide some slack, without allowing commitments into the far future. To avoid probing (should a payer_key become public in some way), we -require a signature for recurring invoice requests; this ensures that -no third party can determine how many invoices have been paid already. +require a signature; this ensures that no third party can determine +how many invoices have been paid already in the case of recurring +requests, and disallows replacement of old invoices by third parties. `payer_info` might typically contain information about the derivation of the `payer_key`. This should not leak any information (such as using a simple @@ -674,6 +683,12 @@ any). Note that for recurring invoices with `proportional_amount` set, the `amount` in the invoice request will be scaled by the time in the period; the sender should not attempt to scale it. +`replace_invoice` allows the mutually-agreed removal of and old unpaid +invoice; this can be used in the case of stuck payments. If +successful in replacing the stuck invoice, the sender may make a +second payment such that it can prove double-payment should the +receiver still accept the first, delayed payment. + # Invoices Invoices are a request for payment, and when the payment is made they @@ -753,6 +768,9 @@ using `onion_message` `invoice` field. 1. type: 52 (`refund_signature`) 2. data: * [`bip340sig`:`payer_signature`] + 1. type: 56 (`replace_invoice`) + 2. data: + * [`sha256`:`payment_hash`] 1. type: 240 (`signature`) 2. data: * [`bip340sig`:`sig`] @@ -833,6 +851,7 @@ A writer of an invoice: - MUST set (or not set) `recurrence_start` exactly as the invoice_request did. - MUST set `payer_key` exactly as the invoice_request did. - MUST set (or not set) `payer_info` exactly as the invoice_request did. + - MUST set (or not set) `replace_invoice` exactly as the invoice_request did. - MUST begin `description` with the `description` from the offer. - MAY append additional information to `description` (e.g. " +shipping"). - if it does not set `amount` to the *base invoice amount* calculated from the invoice_request: From f5cb1709e20e01e641e84d1c900b3276dc46a4d1 Mon Sep 17 00:00:00 2001 From: niftynei Date: Tue, 11 May 2021 15:35:10 -0500 Subject: [PATCH 9/9] offers: add scheme for required delivery info Provide lightweight mechanism for peer to send info about the delivery of paid goods to the peer. Lightly structured, with provided extensibility --- 12-offer-encoding.md | 62 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/12-offer-encoding.md b/12-offer-encoding.md index 7ebd71bd1..1e8d98ff5 100644 --- a/12-offer-encoding.md +++ b/12-offer-encoding.md @@ -246,6 +246,9 @@ The human-readable prefix for offers is `lno`. 1. type: 34 (`refund_for`) 2. data: * [`sha256`:`refunded_payment_hash`] + 1. type: 58 (`req_delivery_info`) + 2. data: + * [`tu64`:`req_info_typeset`] 1. type: 240 (`signature`) 2. data: * [`bip340sig`:`sig`] @@ -466,6 +469,8 @@ A writer of an offer: invoice. - otherwise: - MUST NOT set `refunded_payment_hash`. + - if `req_delivery_info` is present: + - `invoice_request` MUST include data for the required delivery info. A reader of an offer: - if `features` contains unknown _odd_ bits that are non-zero: @@ -494,6 +499,19 @@ It's quite reasonable to set a `recurrence_paywindow` with seconds_after equal to 0, but obviously this should not apply to the initial period if there is no recurrence_base. +The following `req_delivery_info` types are specified, and map to the +given types in the `invoice_request` + +| Bit Position | Invoice Request TLV Type No. | Field | +| ------------- | ----------------------------- | ------------------------ | +| 0 | 70 | Email | +| 1 | 72 | Postal Address | +| 2 | 74 | Phone Number | +| 3 | 76 | Node Id | +| 4 | 78 | Bitcoin address | +| 5 | 80 | SHA256 Hash | +| 6 | 82 | Proprietary/Unstructured | + # Invoice Requests Invoice Requests are a request for an invoice; the human-readable prefix for @@ -533,6 +551,30 @@ invoices is `lnr`. 1. type: 56 (`replace_invoice`) 2. data: * [`sha256`:`payment_hash`] + 1. type: 70 (`delivery_info_email`) + 2. data: + * [`...*byte`:`blob`] + 1. type: 72 (`delivery_info_postal`) + 2. data: + * [`...*byte`:`blob`] + 1. type: 74 (`delivery_info_phone`) + 2. data: + * [`...*byte`:`blob`] + 1. type: 76 (`delivery_info_node`) + 2. data: + * [`node_id`:`node_id`] + 1. type: 78 (`delivery_info_addr`) + 2. data: + * [`...*byte`:`blob`] + 1. type: 80 (`delivery_info_sha256`) + 2. data: + * [`sha256`:`hash`] + 1. type: 82 (`delivery_info_prop`) + 2. data: + * [`...*byte`:`blob`] + 2. data: + * [`u64`:`type`] + * [`...*byte`:`blob`] 1. type: 242 (`payer_signature`) 2. data: * [`bip340sig`:`sig`] @@ -590,6 +632,20 @@ The writer of an invoice_request: - otherwise: - MUST NOT set `recurrence_counter`. - MUST NOT set `recurrence_start` + - if `offer` signaled bit position 0 in `req_delivery_info`: + - MUST set `delivery_info_email` with a valid email address. + - if `offer` signaled bit position 1 in `req_delivery_info`: + - MUST set `delivery_info_postal` with a postal address. + - if `offer` signaled bit position 2 in `req_delivery_info`: + - MUST set `delivery_info_phone` with a phone number. + - if `offer` signaled bit position 3 in `req_delivery_info`: + - MUST set `delivery_info_node` with a valid node id. + - if `offer` signaled bit position 4 in `req_delivery_info`: + - MUST set `delivery_info_addr` with a valid Bitcoin address. + - if `offer` signaled bit position 5 in `req_delivery_info`: + - MUST set `delivery_info_sha256` with a sha256. + - if `offer` signaled bit position 6 in `req_delivery_info`: + - MUST set `delivery_info_prop` with the requested data. The reader of an invoice_request: - MUST fail the request if `payer_key` is not present. @@ -652,6 +708,10 @@ The reader of an invoice_request: of the previous period. - otherwise (the offer had no `recurrence`): - MUST fail the request if there is a `recurrence_counter` field. + - MUST fail the request if any `req_delivery_info` fields are not present + or are present but invalid. + - SHOULD include any provided `req_delivery_info` in the resulting `invoice` + `description`. ## Rationale @@ -1006,7 +1066,7 @@ sender can send a new invoice. # FIXME: Possible future extensions: -1. The offer can require delivery info in the `invoice_request`. +1. ~The offer can require delivery info in the `invoice_request`.~ 2. An offer can be updated: the response to an `invoice_request` is another offer, perhaps with a signature from the original `node_id` 3. Any empty TLV fields can mean the value is supposed to be known by