channeldb: add tombstone option and isOutpointClosed helper
What changed, and why it matters
This commit only adds unused infrastructure for a future feature. It introduces a new option flag, a database field, and a small helper function to check whether a channel has been closed, but nothing is actually wired up or used yet. Behavior is unchanged, and there is no active security issue in this patch.
No action required for this commit. Monitor the follow-up commits that wire the tombstone close path and audit reader usage for consistency with existing close-path semantics.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds OptionTombstoneClosedChannels, a tombstoneClosedChannels field on ChannelStateDB, and an isOutpointClosed helper that reads the existing outpointBucket indexStatus TLV. The helper has no callers, the option defaults to off, and the close-path integration is explicitly deferred to subsequent commits. The change is purely preparatory.
Changed components
channeldb/channel.gochanneldb/db.gochanneldb/options.goInspect captured patch +60 / −1
diff --git a/channeldb/channel.go b/channeldb/channel.go
index c842050..973ea22 100644
--- a/channeldb/channel.go
+++ b/channeldb/channel.go
@@ -368,6 +368,37 @@ const (
outpointClosed indexStatus = 1
)
+// isOutpointClosed reports whether the supplied chanKey has been flipped to
+// outpointClosed in the supplied outpointBucket. The flip is performed in the
+// same transaction as the rest of CloseChannel (sync and tombstone paths
+// alike), so a true result is the authoritative "this channel went through
+// CloseChannel" signal. On tombstone-enabled backends the chanBucket may still
+// exist on disk; readers consult this helper to skip those entries. Callers
+// fetch outpointBucket once and pass it in, which lets loop-style readers
+// hoist the bucket lookup out of the inner loop.
+func isOutpointClosed(opBucket kvdb.RBucket, chanKey []byte) (bool, error) {
+ if opBucket == nil {
+ return false, nil
+ }
+ raw := opBucket.Get(chanKey)
+ if raw == nil {
+ return false, nil
+ }
+
+ var status uint8
+ statusRecord := tlv.MakePrimitiveRecord(indexStatusType, &status)
+ stream, err := tlv.NewStream(statusRecord)
+ if err != nil {
+ return false, err
+ }
+ if err := stream.Decode(bytes.NewReader(raw)); err != nil {
+ return false, fmt.Errorf("decode outpoint status for "+
+ "chan_key=%x: %w", chanKey, err)
+ }
+
+ return indexStatus(status) == outpointClosed, nil
+}
+
// ChannelType is an enum-like type that describes one of several possible
// channel types. Each open channel is associated with a particular type as the
// channel type may determine how higher level operations are conducted such as
diff --git a/channeldb/db.go b/channeldb/db.go
index 999d2cd..d1a7ad0 100644
--- a/channeldb/db.go
+++ b/channeldb/db.go
@@ -418,7 +418,8 @@ func CreateWithBackend(backend kvdb.Backend, modifiers ...OptionModifier) (*DB,
linkNodeDB: &LinkNodeDB{
backend: backend,
},
- backend: backend,
+ backend: backend,
+ tombstoneClosedChannels: opts.tombstoneClosedChannels,
},
clock: opts.clock,
dryRun: opts.dryRun,
@@ -548,6 +549,12 @@ type ChannelStateDB struct {
// backend points to the actual backend holding the channel state
// database. This may be a real backend or a cache middleware.
backend kvdb.Backend
+
+ // tombstoneClosedChannels is set by OptionTombstoneClosedChannels.
+ // When true, CloseChannel skips deleting nested per-channel state and
+ // relies on the outpointBucket flip to outpointClosed as the
+ // authoritative closed-channel signal.
+ tombstoneClosedChannels bool
}
// GetParentDB returns the "main" channeldb.DB object that is the owner of this
diff --git a/channeldb/options.go b/channeldb/options.go
index a8ec8cf..eec3b54 100644
--- a/channeldb/options.go
+++ b/channeldb/options.go
@@ -71,6 +71,16 @@ type Options struct {
// storeFinalHtlcResolutions determines whether to persistently store
// the final resolution of incoming htlcs.
storeFinalHtlcResolutions bool
+
+ // tombstoneClosedChannels, when true, instructs CloseChannel to skip
+ // the cascading deletion of nested per-channel state and rely on the
+ // outpoint-index flip to mark the channel as closed. KV-over-SQL
+ // backends (sqlite, postgres) opt in because nested-bucket deletes
+ // inside a write transaction translate into a long-running
+ // ON DELETE CASCADE that holds the database write-lock for many
+ // seconds on long-lived channels. bbolt and etcd leave this off; the
+ // synchronous delete is already cheap there.
+ tombstoneClosedChannels bool
}
// DefaultOptions returns an Options populated with default values.
@@ -151,3 +161,14 @@ func OptionGcDecayedLog(noGc bool) OptionModifier {
o.OptionalMiragtionConfig.MigrationFlags[1] = !noGc
}
}
+
+// OptionTombstoneClosedChannels controls whether CloseChannel skips the
+// cascading deletion of nested per-channel state and relies on the
+// outpoint-index flip to mark the channel as closed. Set this to true on
+// KV-over-SQL backends (sqlite, postgres); leave it false for bbolt and
+// etcd.
+func OptionTombstoneClosedChannels(enabled bool) OptionModifier {
+ return func(o *Options) {
+ o.tombstoneClosedChannels = enabled
+ }
+}
Why this scored 15/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.