diff --git a/.aspell.en.pws b/.aspell.en.pws index c9e3a0206..6c4ddd1d7 100644 --- a/.aspell.en.pws +++ b/.aspell.en.pws @@ -386,3 +386,13 @@ csv CHECKSIGVERIFY IFDUP sats +onionmsg +Schnorr +unblinded +Merkle +whitespace +rata +programatically +parsable +TLVs +AUD diff --git a/01-messaging.md b/01-messaging.md index 678393e1b..6c030fd3d 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. @@ -231,9 +231,11 @@ 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). +* `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 diff --git a/04-onion-routing.md b/04-onion-routing.md index 5677a1427..3a066a12a 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,96 @@ 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. 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: + 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..5f8f46690 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,70 @@ 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 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 +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 diff --git a/12-offer-encoding.md b/12-offer-encoding.md new file mode 100644 index 000000000..1e8d98ff5 --- /dev/null +++ b/12-offer-encoding.md @@ -0,0 +1,1079 @@ +# 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 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 + 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 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 + 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 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 +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: 58 (`req_delivery_info`) + 2. data: + * [`tu64`:`req_info_typeset`] + 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 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. + +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`. + - 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: + - 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. + +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 +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: 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`] + +## 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. + - 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. + - 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 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`. + - 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. + - 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_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. + - 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. + - 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. + - 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 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. + - 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 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 + +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; 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 +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. + +`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 +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: 56 (`replace_invoice`) + 2. data: + * [`sha256`:`payment_hash`] + 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 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: + - 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 `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 + 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 + } + } +] 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' \