docs: release note for closed-channel tombstones on KV-SQL backends
What changed, and why it matters
This commit only adds release-note documentation describing a previously merged code change (PR #10780). It explains that, on sqlite/postgres backends, closing a channel now leaves the old channel data on disk and instead marks the channel as closed by flipping an index entry. The note warns operators not to downgrade after closing channels, because older LND versions would see the leftover data and think the channel is still open. There is no code change here, only documentation, so it does not introduce a new security vulnerability by itself. The underlying design change could create operational confusion (channels appearing reopened after a downgrade), but that is a documented operational risk, not an exploitable weakness.
No code-level action is required for this documentation commit. Operators running sqlite/postgres backends should heed the downgrade warning and treat 0.21 as a one-way upgrade for databases where channels have been closed. Reviewers may want to verify that PR #10780 indeed updated all open-channel readers to check the outpointClosed marker, and that migration tooling will eventually reclaim the retained tombstone data. If evaluating security risk, focus on the operational downgrade scenario rather than a remote exploit.
Security signals we found
Operational downgrade hazard: closed channels may reappear as open if a node operator downgrades to a pre-0.21 release on sqlite/postgres
Data retention change: closed-channel state (chanBucket, revocation log, forwarding packages) remains on disk for the channel's lifetime on KV-SQL backends
Potential inconsistency in PendingChannels: preserved forwarding-package bucket causes NumForwardingPackages divergence
Performance/availability side effect documented: previous cascading delete could hold DB write-lock long enough to stall HTLC forwarding and trigger force-closes
Evidence from the diff
The diff is a documentation-only addition to docs/release-notes/release-notes-0.21.0.md. It documents PR #10780, which changed the channel-close path on kvdb-over-SQL (sqlite/postgres) backends. Instead of deleting the nested chanBucket (which caused long cascading deletes of revocation logs and forwarding packages), the close path now flips the outpoint index from outpointOpen to outpointClosed and leaves the bucket data in place. All open-channel readers were updated to check that marker. The release note adds a downgrade warning: pre-0.21 binaries do not consult outpointClosed, so downgrading after a close on sqlite/postgres would resurrect the channel as open in listchannels, pendingchannels, and chain-watch. bbolt/etcd still delete the bucket synchronously and are unaffected. No executable code is modified in this commit.
Changed components
docs/release-notes/release-notes-0.21.0.mdLND channel database (kvdb sqlite/postgres backends)channel close path (CloseChannel)open-channel readers that consult outpointOpen/outpointClosed indexInspect captured patch +31 / −0
diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md
index b8a3778..a981a70 100644
--- a/docs/release-notes/release-notes-0.21.0.md
+++ b/docs/release-notes/release-notes-0.21.0.md
@@ -285,6 +285,37 @@
public-only path sees a ~42% speedup; on the previous code it could stall
for minutes.
+* [Tombstone closed channels on KV-over-SQL
+ backends](https://github.com/lightningnetwork/lnd/pull/10780). Closing a
+ long-lived channel previously issued a single `DeleteNestedBucket` inside
+ the close transaction. On the kvdb-on-SQL schema (sqlite, postgres) that
+ delete fans out into a row-by-row `ON DELETE CASCADE` over the channel's
+ revocation log and forwarding-package bucket, holding the database
+ write-lock for many seconds — long enough on channels with millions of
+ states to stall HTLC forwarding, time out htlcswitch retries, and trigger
+ force-close cycles. `CloseChannel` now skips the cascading delete on
+ these backends; the outpoint-index flip from `outpointOpen` to
+ `outpointClosed` (already performed by the existing close path) is the
+ authoritative closed-channel marker, and every reader of the open-channel
+ bucket consults it before treating a channel as open. The bulk historical
+ state — the chanBucket itself, the revocation log, and the per-channel
+ forwarding-package bucket — remains on disk for the channel's lifetime in
+ this database and is reclaimed wholesale by the upcoming native-SQL
+ channel-state migration. bbolt and etcd retain the synchronous one-shot
+ close path, where nested-bucket deletion is already cheap.
+
+ > ⚠️ **Downgrade warning.** On sqlite/postgres, once a channel is
+ > closed under this build the chanBucket and its nested state remain
+ > on disk; the close is signalled only by the `outpointClosed` flip
+ > in the outpoint index. Earlier `lnd` releases do not consult that
+ > flip when iterating `openChannelBucket`, so downgrading to a
+ > pre-0.21 binary after closing channels on these backends will
+ > resurrect those channels as open in `listchannels`,
+ > `pendingchannels`, and the chain-watch path. Operators who close
+ > channels on sqlite/postgres after upgrading should treat the
+ > upgrade as one-way for that database; bbolt and etcd users are unaffected
+ > because the close path on those backends still deletes the chanBucket.
+
## Deprecations
### ⚠️ **Warning:** Deprecated fields in `lnrpc.Hop` will be removed in release version **0.22**
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.