Repository navigation
bLIP-57: Channel Lease Extensions (LSPS7) #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,317 @@ | ||
| ``` | ||
| bLIP: 57 | ||
| Title: LSPS7: Channel Lease Extensions | ||
| Status: Draft | ||
| Author: Evan Kaloudis <zeusln@tutanota.com> | ||
| Created: 2025-01-10 | ||
| License: MIT | ||
| ``` | ||
|
|
||
| # Channel Lease Extensions (LSPS7) | ||
|
|
||
| LSPS7 requires [LSPS0](./blip-0050.md) and therefore [BOLT8](https://github.com/lightning/bolts/blob/master/08-transport.md) as a transport layer. The payment object structure is taken from [LSPS1][]. | ||
|
|
||
| ## Motivation | ||
|
|
||
| The goal of this specification is to provide a standardized LSP API for wallets to extend the lifetime of a channel purchased from an LSP. | ||
|
|
||
| LSPS7 applies to any channel between the client and the LSP for which the LSP has committed to keep the channel open until a certain block height. This includes channels purchased via [LSPS1][], channels opened via [LSPS2][], and channels opened by other means. The lease is a property of the channel as a whole. See [6. Splicing](#6-splicing). | ||
|
|
||
| ## Copyright | ||
|
|
||
| This bLIP is licensed under the MIT license. | ||
|
|
||
| ## Order Flow Overview | ||
|
|
||
| * Client calls `lsps7.get_extendable_channels` to get the LSP's options. | ||
| * Client calls `lsps7.create_order` to create an order. | ||
| * Client pays the order either on-chain or off-chain. | ||
| * LSP extends the expiry time of the channel as soon as the payment is confirmed. | ||
|
|
||
|
|
||
| ## API | ||
|
|
||
| ### 1. lsps7.get_extendable_channels | ||
|
|
||
| | JSON-RPC Method | lsps7.get_extendable_channels | | ||
| |---------------- |----------- | | ||
| | Idempotent | Yes | | ||
|
|
||
|
|
||
| `lsps7.get_extendable_channels` is the entrypoint for each client using the API. It lists all options in a dictionary. | ||
|
|
||
| The client SHOULD call `lsps7.get_extendable_channels` first. | ||
|
|
||
| **Request** No parameters needed. | ||
|
|
||
| **Response** | ||
|
|
||
| ```JSON | ||
| { | ||
| "extendable_channels": [ | ||
| { | ||
| "original_order": { | ||
| "id": "3c2ef2b-195f-0695-b72ef-3929b9138e04", | ||
| "service": "LSPS1" | ||
| }, | ||
| "extension_order_ids": [ | ||
| "142b3ef8-2eca-6135-7f74-7d1afda8b835", | ||
| "39afg2a3-7cf7-f3b6-056e-43f0d5b61f3f" | ||
| ], | ||
| "short_channel_id": "871428x964x0", | ||
| "max_channel_extension_expiry_blocks": 300, | ||
| "expiration_block": 839230 | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| `extendable_channels` is an array of channels the LSP will allow you to extend the expiry of. The structure of the `extendable_channel` object can be found in section [3. Extendable Channel](#3-extendable-channel). | ||
|
|
||
| **LSP** | ||
| - MUST only return channels between the client and the LSP. | ||
|
|
||
|
|
||
| **Errors** No additional errors are defined for this method. | ||
|
|
||
| ### 2. lsps7.create_order | ||
|
|
||
| | JSON-RPC Method | lsps7.create_order | | ||
| |-------------------- |------------------- | | ||
| | Idempotent | No | | ||
|
|
||
|
|
||
| The request is constructed depending on the client's needs. | ||
|
|
||
| **Request** | ||
|
|
||
| ```json | ||
| { | ||
| "short_channel_id": "871428x964x0", | ||
| "channel_extension_expiry_blocks": 144, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to use
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. Either could work. Open to hear arguments in favor of one over the other. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just re-read LSPs1 and now I am convinced it is better to keep |
||
| "token": "", | ||
| "refund_onchain_address": "bc1qvmsy0f3yyes6z9jvddk8xqwznndmdwapvrc0xrmhd3vqj5rhdrrq6hz49h" | ||
| } | ||
| ``` | ||
|
|
||
| - `short_channel_id` <[LSPS0.scid][]> Short Channel Identifier (SCID) of the channel to extend. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to clarify if this can be any SCID of a previously-purchased channel (maybe even ones towards other peers?), or if it needs to be the actual SCID, or the SCID alias, or, in case of bLIP-52 to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarified. The client MUST send an SCID returned by the latest |
||
| - MUST be the `short_channel_id` of one of the channels returned by the most recent `lsps7.get_extendable_channels` call. | ||
| - `channel_extension_expiry_blocks <uint32>` How long to extend the lease of the channel in block time. | ||
| - MUST be 1 or greater. | ||
| - MUST be below or equal to `max_channel_extension_expiry_blocks` returned from the `get_extendable_channels` call. | ||
| - `token <string>` Field for arbitrary data like a coupon code or a authentication token. | ||
| - Client MAY omit this field. | ||
| - `refund_onchain_address <string>` <LSPS0.onchain_address> Address where the LSP will send the funds if the order fails. | ||
| - Client MAY omit this field. | ||
|
|
||
| **Response** | ||
|
|
||
| ```json | ||
| { | ||
| "order_id": "bb4b5d0a-8334-49d8-9463-90a6d413af7c", | ||
| "channel_extension_expiry_blocks": 144, | ||
| "new_channel_expiry_block": 839374, | ||
| "token": "", | ||
| "created_at": "2012-04-23T18:25:43.511Z", | ||
| "order_state": "CREATED", | ||
| "payment": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should also support BOLT12 by now, parallel to bLIP-51 (cf. #59).
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| "bolt11": { | ||
| "state": "EXPECT_PAYMENT", | ||
| "expires_at": "2015-01-25T19:29:44.612Z", | ||
| "fee_total_sat": "8888", | ||
| "order_total_sat": "8888", | ||
| "invoice" : "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05" | ||
| }, | ||
| "bolt12": { | ||
| "state" : "EXPECT_PAYMENT", | ||
| "expires_at": "2015-01-25T19:29:44.612Z", | ||
| "fee_total_sat": "8888", | ||
| "order_total_sat": "8888", | ||
| "offer": "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqvp3pwq2q3shxerxzzfqyxdxxkjqxfpt9tz94zj79m4n99kwjddq92uqryuwsu4nt0t3lthfq02jzqla96dtkf4rew8edxw0p85swe89wd8ldekdl5j262n76qyl2qgzajm08clzr74z6ssy0qlvvp9f5kvrxf27yz4pcy99jge69kxu8ttsqt8gw8jsk5397zvvdf4lfd52paf73thcg6xf57xmvtdrwny5mn2r4jw2d5jzalqrq537mmt6u9qpqytzzql6zemrme07jqqwtza76lldcj9wgc0ccd4d2w584cdcx6szsuupvy" | ||
| }, | ||
| "onchain": { | ||
| "state": "EXPECT_PAYMENT", | ||
| "expires_at": "2015-01-25T19:29:44.612Z", | ||
| "fee_total_sat": "9999", | ||
| "order_total_sat": "9999", | ||
| "address" : "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", | ||
| "min_fee_for_0conf": 253, | ||
| "min_onchain_payment_confirmations": 0, | ||
| "refund_onchain_address": "bc1qvmsy0f3yyes6z9jvddk8xqwznndmdwapvrc0xrmhd3vqj5rhdrrq6hz49h" | ||
| } | ||
| }, | ||
| "channel": { | ||
| "original_order": { | ||
| "id": "3c2ef2b-195f-0695-b72ef-3929b9138e04", | ||
| "service": "LSPS1" | ||
| }, | ||
| "extension_order_ids": [ | ||
| "142b3ef8-2eca-6135-7f74-7d1afda8b835", | ||
| "39afg2a3-7cf7-f3b6-056e-43f0d5b61f3f" | ||
| ], | ||
| "short_channel_id": "871428x964x0", | ||
| "max_channel_extension_expiry_blocks": 300, | ||
| "expiration_block": 839230 | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| - `order_id <string>` Id of this specific order. | ||
| - MUST be unique. | ||
| - MUST be at most 64 characters long. | ||
| - SHOULD be a valid [UUID version 4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)) (aka random UUID). | ||
| - `channel_extension_expiry_blocks <uint32>` Mirrored from the request. | ||
| - `new_channel_expiry_block <uint32>` The block height at which the channel lease will expire once this order is completed. | ||
| - MUST be `channel.expiration_block` + `channel_extension_expiry_blocks`, where `channel.expiration_block` is the value at the time the order was created. | ||
| - `token <string>` Mirrored from the request. | ||
| - MUST be an empty string if the token was not provided. | ||
| - `created_at` <[LSPS0.datetime][]> Datetime when the order was created. | ||
| - `order_state <string enum>` Current state of the order. | ||
| - `CREATED` Order has been created. Default value. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to publish another funding if we extend the channel?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copy-paste from LSPS1, fixed. |
||
| - `COMPLETED` LSP has extended the channel lease. `channel.expiration_block` is equal to `new_channel_expiry_block`. | ||
| - `FAILED` Order failed. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we imagine the user gets reimbursed if they pay via Lightning? Do we still need to clarify that the LSP has to hold the HTLC until they updated the order state fully to make the state transition atomic (although we probably don't need a HOLD state)?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added section 4: the LSP MUST NOT release the preimage before the new expiry is persisted, and if the extension fails it fails the HTLC (that is the refund) and sets |
||
| - `payment <object>` Contains everything about payments, see [4. Payment](#4-payment). | ||
| - `channel <object>` Contains information about the channel, see [3. Extendable Channel](#3-extendable-channel). | ||
|
|
||
|
|
||
| **Client** | ||
| - SHOULD validate that `channel.expiration_block` + `channel_extension_expiry_blocks` = `new_channel_expiry_block`. | ||
| - SHOULD validate the `fee_total_sat` is reasonable. | ||
| - MAY abort the flow here. | ||
|
|
||
| **Errors** | ||
|
|
||
| | Code | Message | Data | Description | | ||
| | ---- | ------- | ----------- | ---- | | ||
| | -32602 | Invalid params | {"property": %invalid_property%, "message": %human_message% } | Invalid method parameter(s). | | ||
| | 001 | Client rejected | {"message": %human_message% } | [LSPS0.client_rejected_error][] | | ||
| | 100 | Option mismatch | {"property": %option_mismatch_property%, "message": %human_message% } | The order doesnt match the options defined in `lsps7.get_extendable_channels` channel object. | | ||
| | 102 | Unrecognized or stale token | {} | The provided token was unrecognized or stale. | | ||
|
|
||
|
|
||
| - LSP MUST validate the order against the options defined in the `lsps7.get_extendable_channels` channel object. LSP MUST return an `100` error in case of a mismatch. | ||
| - `%option_mismatch_property%` MUST be one of the fields in the `lsps7.get_extendable_channels` channel object. | ||
| - Example: `{ "property": "max_channel_extension_expiry_blocks" }`. | ||
|
|
||
| - LSP MUST validate the request fields. LSP MUST return a `-32602` error in case of an invalid request field. | ||
| - `%invalid_property%` MUST be one of the fields in the request body. MUST use `.` to separate nested fields. | ||
| - Example: `{ "property": "channel_extension_expiry_blocks", "message": "Not an integer" }`. | ||
| - LSP MUST return a `-32602` error with `%invalid_property%` set to `short_channel_id` if `short_channel_id` does not identify a channel the LSP would return in `lsps7.get_extendable_channels`. | ||
|
|
||
| - If the client provided a `token`, LSP MUST validate it and return a `102` error if the token is invalid. | ||
|
|
||
| > **Rationale `token` validation** The client should be informed if the token is invalid. Ignoring the invalid token and creating an order without the potentially discount or other side effect is not good UX. Ignoring the invalid token will also NOT prevent anybody bruteforcing the token because the client will still detect if the LSP has given a discount. | ||
|
|
||
| > **Rationale Client rejected** LSPs can reject a client for example for misbehaviour. LSPs can reject a node on two levels: Prevent a peer connection OR disable order creation. Preventing a peer connection might not work in case you still want to allow other functions to keep working, for example an existing channel. | ||
|
|
||
|
|
||
| ### 2.1 lsps7.get_order | ||
|
|
||
| | JSON-RPC Method | lsps7.get_order | | ||
| |---------------- |---------------- | | ||
| | Idempotent | Yes | | ||
|
|
||
| The client MAY check the current status of the order at any point. | ||
|
|
||
| **Request** | ||
|
|
||
| ```json | ||
| { | ||
| "order_id": "bb4b5d0a-8334-49d8-9463-90a6d413af7c" | ||
| } | ||
| ``` | ||
|
|
||
| **Response** is the same as defined in `lsps7.create_order`. | ||
|
|
||
| **Errors** | ||
|
|
||
| | Code | Message | Data | Description | | ||
| | ------ | --------- | ------- | ----------------------------------------------------- | | ||
| | 101 | Not found | {} | Order with the requested order_id has not been found. | | ||
|
|
||
|
|
||
| ### 3. Extendable channel | ||
|
|
||
| The `extendable_channel` object returned in `lsps7.get_extendable_channels`, `lsps7.create_order`, and `lsps7.get_order`, has the following properties: | ||
|
|
||
| - `original_order <object>` An *optional* property for the order through which the channel was originally purchased. | ||
| - `id <string>` The id of that order, as returned by the service it was purchased through. | ||
| - `service <string>` The service the channel was purchased through. MUST be the uppercase name of the LSPS spec, for example `LSPS1`. | ||
| - MUST be omitted if the channel was not opened through an order that has an id, for example channels opened via [LSPS2][] or channels opened by the client. | ||
| - `extension_order_ids <string[]>` An *optional* list of the `order_id`s of previous `lsps7.create_order` orders for this channel, oldest first. | ||
| - MUST only contain orders with `order_state` `COMPLETED`. | ||
| - MUST NOT contain the id of `original_order`. | ||
| - MUST be omitted or empty if the channel lease has not been extended before. | ||
| - `short_channel_id` <[LSPS0.scid][]> Short Channel Identifier (SCID) of the channel. | ||
| - MUST be the SCID of the channel's current funding transaction. After a splice, this is the SCID of the splice transaction. | ||
| - MAY be an SCID alias if the funding transaction is not yet confirmed. | ||
| - MUST NOT be an intercept SCID such as the [LSPS2][] `jit_channel_scid`. | ||
| - `max_channel_extension_expiry_blocks <uint32>` The maximum number of blocks a single `lsps7.create_order` order MAY extend the lease by. | ||
| - `expiration_block <uint32>` The block height at which the channel lease will expire. May be marked as 0 if the user opened the channel to the LSP, and the LSP has no obligation to keep open. | ||
| - MUST be 0 or greater. | ||
| - For channels opened via [LSPS2][], which does not define a lease duration, this is set by LSP policy. | ||
|
|
||
| > **Rationale `original_order`** The client MAY want to look up the original order for reference. [LSPS2][] does not assign order ids, so there is nothing to reference for those channels. The channel is identified by `short_channel_id` alone. | ||
|
|
||
| > **Rationale `extension_order_ids`** The client MAY want to look up any one of the previous extensions orders for reference. | ||
|
|
||
| > **Rationale `short_channel_id`** The client learns the SCID from `lsps7.get_extendable_channels` rather than choosing it, so client and LSP never disagree about which of a channel's identifiers to use. | ||
|
|
||
| ### 4. Payment | ||
|
|
||
| The `payment` object returned by `lsps7.create_order` and `lsps7.get_order`, mirrors the formatting of [LSPS1.payment][], including the `bolt11`, `bolt12`, and `onchain` payment options, with the following changes: | ||
|
|
||
| - `fee_total_sat` <[LSPS0.sat][]> The total fee the LSP will charge to extend the channel lease. | ||
| - `order_total_sat` <[LSPS0.sat][]> What the client needs to pay in total to extend the channel lease. | ||
| - MUST be equal to `fee_total_sat`. | ||
|
|
||
| **LSP** | ||
|
|
||
| For `bolt11` and `bolt12` payments: | ||
| - MUST NOT release the preimage before the new `expiration_block` has been persisted. | ||
| - MUST change the `payment.state` to `HOLD` when the payment arrived. | ||
| - If the channel lease has been extended successfully: | ||
| - MUST release the preimage and therefore complete the payment. | ||
| - MUST set the `payment.state` to `PAID`. | ||
| - If the lease could not be extended, the order expired, or shortly before the payment times out: | ||
| - MUST fail the payment. Failing the payment is the refund. | ||
| - MUST set the `payment.state` to `REFUNDED`. | ||
|
|
||
| For `onchain` payments: | ||
| - If the lease could not be extended, MUST refund the client to `refund_onchain_address`. | ||
|
|
||
| > **Rationale** Persisting the new expiry before releasing the preimage makes the payment and the extension atomic from the client's point of view. Extending a lease is a bookkeeping change on the LSP side, so the payment is expected to stay in `HOLD` only briefly. | ||
|
|
||
| ### 5. Channel Extension | ||
|
|
||
| The LSP MUST extend the channel lease when: | ||
| - `payment.state` is `HOLD` (lightning) or `PAID` (on-chain). | ||
|
|
||
| In case the extension succeeds: | ||
| - MUST set `channel.expiration_block` to `new_channel_expiry_block`. | ||
| - MUST set `order_state` to `COMPLETED`. | ||
| - MUST add `order_id` to `extension_order_ids`. | ||
|
|
||
| In case the extension fails: | ||
| - MUST set `order_state` to `FAILED`. | ||
| - MUST issue a refund as described in [4. Payment](#4-payment). | ||
|
|
||
| ### 6. Splicing | ||
|
|
||
| The lease applies to the channel as a whole, not to a particular portion of its capacity or balance. | ||
|
|
||
| **LSP** | ||
| - MUST NOT change `expiration_block` as a result of a splice. | ||
| - MUST continue to return the channel in `lsps7.get_extendable_channels` after a splice, with `short_channel_id` set to the SCID of the new funding transaction. | ||
|
|
||
| > **Rationale** Tracking separate expiries for liquidity added at different times would make it unclear which part of the balance is leased until when. An LSP that wants to charge for additional liquidity spliced into a leased channel can do so in the order that adds the liquidity. | ||
|
|
||
|
|
||
| [LSPS0.onchain_address]: ./blip-0050.md#link-lsps0onchain_address | ||
| [LSPS0.datetime]: ./blip-0050.md#link-lsps0datetime | ||
| [LSPS0.scid]: ./blip-0050.md#link-lsps0scid | ||
| [LSPS0.sat]: ./blip-0050.md#link-lsps0sat | ||
| [LSPS0.client_rejected_error]: ./blip-0050.md#link-lsps0client_rejected_error | ||
| [LSPS1]: ./blip-0051.md | ||
| [LSPS1.payment]: ./blip-0051.md#3-payment | ||
| [LSPS2]: ./blip-0052.md | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it unfair to pay for the extension of a channel in which the balance has been on the non-LSP side during all of the previous period? No capital cost for the LSP. Or would this be taken into account when pricing the next extension?
I do realize of course that this is more of a remark on the side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's up to the LSP how they want to price it. Pricing basing on the current ratio of balances is a bit tricky as it can change drastically in an instant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current ratio can of course change drastically, but I was thinking of the average balance of the previous period. So even if the final balance is skewed heavily towards the LSP, it may not mean that the average balance, that is the basis for the calculation of the price of the extension, is too.
A continuous model for this would be to have a constant flow of sats towards the LSP proportional to the LSP balance.
Ofc still side remarks all of this 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea for sure