Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 317 additions & 0 deletions blip-0057.md
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.

Copy link
Copy Markdown

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.

Copy link
Copy Markdown
Author

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

Copy link
Copy Markdown

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 😅

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea for sure


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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to use new_channel_expiry_blocks here instead of channel_extension_expiry_blocks ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 channel_extension_expiry_blocks for the sake of uniformity.

"token": "",
"refund_onchain_address": "bc1qvmsy0f3yyes6z9jvddk8xqwznndmdwapvrc0xrmhd3vqj5rhdrrq6hz49h"
}
```

- `short_channel_id` <[LSPS0.scid][]> Short Channel Identifier (SCID) of the channel to extend.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 jit_channel_scid?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified. The client MUST send an SCID returned by the latest lsps7.get_extendable_channels, and that call MUST only return channels between the client and the LSP. The SCID is the one of the current funding transaction (the splice transaction after a splice), MAY be an alias before confirmation, and MUST NOT be an intercept SCID such as jit_channel_scid. An unknown SCID returns -32602 with property: short_channel_id.

- 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": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. payment mirrors LSPS1.payment, so bolt12 was already implied after #59. The example now includes it and section 4 names it explicitly.

"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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy-paste from LSPS1, fixed. COMPLETED now means the lease has been extended and channel.expiration_block equals new_channel_expiry_block.

- `COMPLETED` LSP has extended the channel lease. `channel.expiration_block` is equal to `new_channel_expiry_block`.
- `FAILED` Order failed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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 REFUNDED. I kept HOLD since the payment object comes from LSPS1, but noted it should only last briefly because extending a lease is a bookkeeping change. There's also a new section 5 for the extension itself, mirroring LSPS1's channel open section.

- `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