Skip to content
Open
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions blip-0002.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ bLIPs may reserve feature bits by adding them to the following table:
| 260/261 | `htlc_endorsement` | This node forwards experimental htlc endorsement signals | N | | [bLIP 4](./blip-004.md) |
| 262/263 | `bolt11_blinded_path` | This invoice may contain a new blinded path tagged field | I | `option_route_blinding` | [bLIP 39](./blip-0039.md) |
| 729 | `option_supports_lsps` | This node supports LSPS protocol(s) | IN | | [bLIP 50](./blip-0050.md) |
| 828/829 | `rgb_recovery` | This node implements the RGB Recovery disaster-recovery protocol | IN | `rgb_channel` | [bLIP 72](./blip-0072.md) |

### Messages

Expand All @@ -78,6 +79,9 @@ bLIPs may create new messages and reserve their type in the following table:
| 63501 | `hc_updated_fail_htlc` | [bLIP 17](./blip-0017.md) |
| 63499 | `hc_update_fail_malformed_htlc` | [bLIP 17](./blip-0017.md) |
| 63497 | `hc_error` | [bLIP 17](./blip-0017.md) |
| 60001 | `rgbr_query_channel_close` | [bLIP 72](./blip-0072.md) |
| 60003 | `rgbr_channel_close_info` | [bLIP 72](./blip-0072.md) |
| 60005 | `rgbr_error` | [bLIP 72](./blip-0072.md) |

### TLV fields in BOLT messages

Expand Down
246 changes: 246 additions & 0 deletions blip-0072.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
```
bLIP: 72
Title: RGB-LN Disaster Recovery Protocol
Status: Draft
Author: will-bitlightlabs <jayabb@bitlightlabs.com>
Created: 2026-06-29
License: CC0
Requires: 70
```

## Abstract

This bLIP defines the **RGB Recovery** protocol: an `rgb_recovery` feature bit (829, odd) and three custom messages (types 60001 / 60003 / 60005, odd) that let a party which has lost the local state of an RGB channel query its counterparty for that channel's close status and obtain the download address of the consignment needed to rebuild its own RGB allocation.

It is a sibling to [bLIP-0070](https://github.com/lightning/blips/pull/70) (RGB colored channel support): bLIP-0070 defines how RGB assets travel over Lightning (feature bit 827), while this bLIP defines how to recover the assets after an RGB channel is closed (feature bit 829).

All extensions are odd-typed; standard Lightning nodes ignore them under the "it's ok to be odd" rule and are unaffected.

## Copyright

CC0.

---

## Motivation

After an ordinary Lightning channel closes, a party that has lost its channel state can still sweep its BTC from the `to_remote` output using only its seed, with no communication with the counterparty — this is achievable purely on-chain through key derivation.

RGB colored channels cannot. RGB is a client-side validated protocol: the asset allocation is maintained off-chain as **CSV state** (persisted in the node's Stock), and the chain carries only an OP_RETURN commitment. After a party loses its local state, it **cannot rebuild its own RGB allocation from on-chain data alone** — it must retrieve it from the counterparty that still holds the corresponding CSV state.

RGB Recovery fills this single missing link: it lets the recovering party query the counterparty for the channel's close status, locate the consignment the counterparty exports, and rebuild its RGB allocation from it.

---

## Rationale

### Reusing the RGB feature family (827 → 829)

The number for `rgb_channel` (827) derives from RGB's SLIP-0044 registered coin type 827166/827167. RGB Recovery is the companion recovery capability for RGB channels, so it reuses the adjacent odd bit **829** in the same family, forming an "open channel / recover channel" correspondence with 827.

### 829 is advertised only in Init/Node

RGB Recovery is a **node capability** (whether the node can answer recovery queries). Recovery happens **after** a channel has closed, where there is no channel-type negotiation context. Therefore 829 appears only in `init` / `node_announcement` and not in `channel_type`.

### consignment travels by URL, not inside the LN message

A consignment grows with the channel's transfer history (the operation DAG) and is not compressed; it can reach MB or even GB scale, far too large to carry inside an LN message. `RGBR_CHANNEL_CLOSE_INFO` returns only a `consignment_url`, by which the recovering party downloads it out-of-band. The URL's concrete scheme is left to the implementation and is not constrained by this protocol.

---

## Specification

### Feature Bit

| Bits | Name | Description | Context | Dependencies |
|---|---|---|---|---|
| 828/829 | `rgb_recovery` | The node implements RGB Recovery and can answer `RGBR_QUERY_CHANNEL_CLOSE`. | `init`, `node_announcement` | `rgb_channel` (827) |

- **829** (odd, optional): used in `init` and `node_announcement`. A counterparty lacking this bit does not affect the handshake; it merely signals that the counterparty does not answer recovery queries.
- Not used in `channel_type`.
- Depends on `rgb_channel` (827).

### Custom Message Types

RGB Recovery uses odd types (BOLT-1: odd = optional; an unsupporting counterparty ignores rather than disconnects). The three messages are named with the `RGBR_` prefix (short for RGB Recovery).

| Message | Type | Direction | Description |
|---|---|---|---|
| `RGBR_QUERY_CHANNEL_CLOSE` | **60001** (0xEA61) | recovering party → counterparty | Query a channel's close status + consignment |
| `RGBR_CHANNEL_CLOSE_INFO` | **60003** (0xEA63) | counterparty → recovering party | Response |
| `RGBR_ERROR` | **60005** (0xEA65) | counterparty → recovering party | Error response |

The range `60001-60099` (odd) is reserved for future extensions of the RGB Recovery family.

### Message TLV Fields

Field types follow the [BOLT-1 fundamental types](https://github.com/lightning/bolts/blob/master/01-messaging.md#fundamental-types) (`byte` / `u16` / `u64` / `sha256` / `channel_id` / `utf8`). `utf8` and `byte` are encoded bare in the TLV value: the length is given by the TLV record's `length`, and the value contains no extra length prefix or trailing bytes.

Each message's payload is a single TLV stream (the message's `data` carries one `tlvs` field, defined below). Whether a given field is required is specified in [Requirements](#requirements).

#### `RGBR_QUERY_CHANNEL_CLOSE` (60001)

1. type: 60001 (`rgbr_query_channel_close`)
2. data:
* [`rgbr_query_channel_close_tlvs`:`tlvs`]

1. `tlv_stream`: `rgbr_query_channel_close_tlvs`
2. types:
1. type: 0 (`protocol_version`)
2. data:
* [`u16`:`protocol_version`]
1. type: 2 (`query_id`)
2. data:
* [`u64`:`query_id`]
1. type: 4 (`channel_id`)
2. data:
* [`channel_id`:`channel_id`]
1. type: 6 (`funding_txid`)
2. data:
* [`sha256`:`funding_txid`]

#### `RGBR_CHANNEL_CLOSE_INFO` (60003)

1. type: 60003 (`rgbr_channel_close_info`)
2. data:
* [`rgbr_channel_close_info_tlvs`:`tlvs`]

1. `tlv_stream`: `rgbr_channel_close_info_tlvs`
2. types:
1. type: 0 (`protocol_version`)
2. data:
* [`u16`:`protocol_version`]
1. type: 2 (`query_id`)
2. data:
* [`u64`:`query_id`]
1. type: 4 (`channel_id`)
2. data:
* [`channel_id`:`channel_id`]
1. type: 6 (`status`)
2. data:
* [`byte`:`status`]
1. type: 8 (`close_txid`)
2. data:
* [`sha256`:`close_txid`]
1. type: 10 (`consignment_url`)
2. data:
* [`...*byte`:`consignment_url`]
1. type: 12 (`rgb_amount`)
2. data:
* [`u64`:`rgb_amount`]
1. type: 14 (`peer_health`)
2. data:
* [`byte`:`peer_health`]

#### `RGBR_ERROR` (60005)

1. type: 60005 (`rgbr_error`)
2. data:
* [`rgbr_error_tlvs`:`tlvs`]

1. `tlv_stream`: `rgbr_error_tlvs`
2. types:
1. type: 0 (`protocol_version`)
2. data:
* [`u16`:`protocol_version`]
1. type: 2 (`query_id`)
2. data:
* [`u64`:`query_id`]
1. type: 4 (`error_code`)
2. data:
* [`byte`:`error_code`]
1. type: 6 (`error_message`)
2. data:
* [`...*byte`:`error_message`]

### Enumerated Values

The following `status` values are defined:

* `0`: `Closed` — the counterparty has force-closed; `close_txid` + `consignment_url` are required
* `1`: `Open` — the counterparty considers the channel still open (state divergence)
* `2`: `Unknown` — the counterparty has forgotten the channel (already cleaned up)

The following `peer_health` values are defined:

* `0`: `Healthy` — the counterparty is operating normally
* `1`: `Recovering` — the counterparty is itself in recovery; its answer may be unreliable
* `2`: `Unknown` — the counterparty cannot determine its own state

The following `error_code` values are defined:

* `0`: `unsupported_version` — unrecognized `protocol_version`
* `1`: `unknown_channel` — no matching channel found
* `2`: `channel_not_closed` — the channel exists but is not closed
* `3`: `consignment_unavailable` — the channel is closed but the consignment cannot be provided
* `4`: `peer_not_authorized` — the counterparty refuses to provide the information (policy)
* `5`: `internal_error` — counterparty internal error

### State Machine

```
recovering party counterparty
│ │
│ ── RGBR_QUERY_CHANNEL_CLOSE ─────► │ (query_id=N)
│ │
│ ◄──── RGBR_CHANNEL_CLOSE_INFO ──── │ (query_id=N, status, [close_txid,
│ │ consignment_url], peer_health)
│ or │
│ ◄──────── RGBR_ERROR ───────────── │ (query_id=N, error_code)
```

- Single request / single response, with no subsequent state.
- The response MUST echo the request's `query_id` (by which the recovering party correlates concurrent queries).
- Retry on failure is decided by the caller; the protocol has no built-in retry.

### Version Negotiation

- Every message carries `protocol_version: u16` as TLV type 0; currently = **1**.
- A receiver that receives an unrecognized version replies with `RGBR_ERROR(unsupported_version)`.
- Backward-compatible extensions **add odd TLV fields** (BOLT-1: an unknown odd TLV is ignored, an unknown even TLV is treated as an error); breaking changes increment `protocol_version`. New fields therefore always use odd types, so older nodes can safely ignore them without erroring.

### Requirements

**`funding_txid` / `close_txid` byte order**: BOLT internal/wire byte order (i.e. consensus encoding, as in BOLT-2 `funding_txid`), **not** the block-explorer/RPC display order.

**Responder (the counterparty constructing `RGBR_CHANNEL_CLOSE_INFO`)**:
- When `status = Closed`, MUST set `close_txid` and `consignment_url`.
- When `status = Open` or `Unknown`, MUST NOT set `close_txid` / `consignment_url` / `rgb_amount`.
- MUST always set `peer_health`.
- MUST echo the request's `query_id`.
- `consignment_url` MUST be ≤ 1024 bytes; `error_message` MUST be ≤ 1024 bytes.

**Receiver**:
- If any required TLV field of that message is missing, MUST treat the message as invalid and discard it.
- If `status = Closed` but `close_txid` or `consignment_url` is missing, MUST treat the message as invalid.
- An unknown odd TLV type MUST be ignored; an unknown even TLV type MUST be treated as an error (BOLT-1).

**Recovering party (initiating `RGBR_QUERY_CHANNEL_CLOSE`)**:
- SHOULD keep `query_id` unique among in-flight requests, to correlate the responses of concurrent queries.

---

## Universality

RGB Recovery is entirely optional. `rgb_recovery` (829) is an odd bit; a standard node ignores it in `init` and the handshake is unaffected. All three custom messages are odd-typed; an unsupporting node ignores them under BOLT-1 and does not disconnect.

## Backwards Compatibility

| Extension | Parity | Standard node behavior |
|---|---|---|
| feature bit 829 | odd (optional) | Ignored in `init`; normal handshake |
| custom message 60001/60003/60005 | odd | Ignored on receipt; no disconnect |

Depends on bLIP-0070 (827).

## Reference Implementation

A reference implementation based on LDK (rust-lightning) is under development by Bitlight Labs and will be made available upon public release.


---

## Limitations

- **Double-blind disaster**: when both parties to a channel have lost their state, neither retains the latest CSV state (Stock), so the RGB portion cannot be recovered (`peer_health=Recovering` serves to warn of this).
- **in-flight HTLC**: the RGB balance already settled at channel close can be recovered normally; but the BTC and RGB carried by an HTLC still in flight (not yet settled) at the moment of close are out of scope for this version — they remain on the commitment's HTLC outputs and require a preimage or timeout to be claimed via the HTLC-claim path, which this version does not implement.