From b59807f24a80b21597333209c02a41eecf0a1c34 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 21:39:52 -0300 Subject: [PATCH 01/61] chanstate: move funding tx presence helper Move the funding transaction presence check onto OpenChannel so the decision lives with the channel-state domain type instead of the KV serialization helpers in channeldb. This keeps the existing channeldb encoding behavior unchanged while removing one backend-independent helper from the KV package. --- channeldb/channel.go | 14 ++------------ chanstate/open_channel.go | 10 ++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 135565c7c4..91daa5430c 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3170,16 +3170,6 @@ func writeChanConfig(b io.Writer, c *ChannelConfig) error { ) } -// fundingTxPresent returns true if expect the funding transcation to be found -// on disk or already populated within the passed open channel struct. -func fundingTxPresent(channel *OpenChannel) bool { - chanType := channel.ChanType - - return chanType.IsSingleFunder() && chanType.HasFundingTx() && - channel.IsInitiator && - !channel.HasChanStatusForStore(ChanStatusRestored) -} - func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { var w bytes.Buffer if err := WriteElements(&w, @@ -3195,7 +3185,7 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { // For single funder channels that we initiated, and we have the // funding transaction, then write the funding txn. - if fundingTxPresent(channel) { + if channel.FundingTxPresent() { if err := WriteElement(&w, channel.FundingTxn); err != nil { return err } @@ -3379,7 +3369,7 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { // For single funder channels that we initiated and have the funding // transaction to, read the funding txn. - if fundingTxPresent(channel) { + if channel.FundingTxPresent() { if err := ReadElement(r, &channel.FundingTxn); err != nil { return err } diff --git a/chanstate/open_channel.go b/chanstate/open_channel.go index 2011ebff48..b946b749ed 100644 --- a/chanstate/open_channel.go +++ b/chanstate/open_channel.go @@ -431,6 +431,16 @@ func (c *OpenChannel) BroadcastHeight() uint32 { return c.FundingBroadcastHeight } +// FundingTxPresent returns true if expect the funding transcation to be found +// on disk or already populated within the passed open channel struct. +func (c *OpenChannel) FundingTxPresent() bool { + chanType := c.ChanType + + return chanType.IsSingleFunder() && chanType.HasFundingTx() && + c.IsInitiator && + !c.HasChanStatusForStore(ChanStatusRestored) +} + // SetBroadcastHeight sets the FundingBroadcastHeight. func (c *OpenChannel) SetBroadcastHeight(height uint32) { c.Lock() From 6d054891aad05a727040ae75c29243e34697b979 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 21:56:57 -0300 Subject: [PATCH 02/61] chanstate: move forwarding kv package Move the current forwarding-package KV implementation into chanstate/kv_forwarding_package.go and leave channeldb with the temporary compatibility aliases used by existing callers. This keeps the forwarding-package store as its own review slice and leaves migration code untouched. --- channeldb/channel_test.go | 2 +- channeldb/forwarding_package.go | 745 ++-------------------------- chanstate/kv_forwarding_package.go | 747 +++++++++++++++++++++++++++++ chanstate/kv_serialization.go | 62 +++ 4 files changed, 843 insertions(+), 713 deletions(-) create mode 100644 chanstate/kv_forwarding_package.go create mode 100644 chanstate/kv_serialization.go diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index c955ea96b1..6d149160de 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -1439,7 +1439,7 @@ func TestRefresh(t *testing.T) { } require.Equal( - t, chanOpenLoc, NewChannelPackager(state.ShortChanID()).source, + t, chanOpenLoc, NewChannelPackager(state.ShortChanID()).Source(), ) } diff --git a/channeldb/forwarding_package.go b/channeldb/forwarding_package.go index 6b9dfd3f50..08d85bf1fe 100644 --- a/channeldb/forwarding_package.go +++ b/channeldb/forwarding_package.go @@ -1,12 +1,7 @@ package channeldb import ( - "bytes" - "errors" - cstate "github.com/lightningnetwork/lnd/chanstate" - "github.com/lightningnetwork/lnd/kvdb" - "github.com/lightningnetwork/lnd/lnwire" ) type ( @@ -27,6 +22,30 @@ type ( // FwdPkg records all adds, settles, and fails that were locked in as a // result of the remote peer sending us a revocation. FwdPkg = cstate.FwdPkg + + // SettleFailAcker is a generic interface providing the ability to + // acknowledge settle/fail HTLCs stored in forwarding packages. + SettleFailAcker = cstate.SettleFailAcker + + // GlobalFwdPkgReader is an interface used to retrieve the forwarding + // packages of any active channel. + GlobalFwdPkgReader = cstate.GlobalFwdPkgReader + + // FwdOperator defines the interfaces for managing forwarding packages + // that are external to a particular channel. + FwdOperator = cstate.FwdOperator + + // FwdPackager supports all operations required to modify fwd packages, + // such as creation, updates, reading, and removal. + FwdPackager = cstate.FwdPackager + + // SwitchPackager is a concrete implementation of the FwdOperator + // interface. + SwitchPackager = cstate.SwitchPackager + + // ChannelPackager is used by a channel to manage the lifecycle of its + // forwarding packages. + ChannelPackager = cstate.ChannelPackager ) const ( @@ -43,6 +62,10 @@ const ( ) var ( + // fwdPackagesKey is retained while the root channeldb bucket setup + // remains in this package. + fwdPackagesKey = cstate.FwdPackagesBucketKey() + // NewPkgFilter initializes an empty PkgFilter supporting `count` // elements. NewPkgFilter = cstate.NewPkgFilter @@ -52,713 +75,11 @@ var ( // ErrCorruptedFwdPkg signals that the on-disk structure of the // forwarding package has potentially been mangled. - ErrCorruptedFwdPkg = errors.New("fwding package db has been corrupted") + ErrCorruptedFwdPkg = cstate.ErrCorruptedFwdPkg - // fwdPackagesKey is the root-level bucket that all forwarding packages - // are written. This bucket is further subdivided based on the short - // channel ID of each channel. - // - // Bucket hierarchy: - // - // fwdPackagesKey(root-bucket) - // | - // |-- - // | | - // | |-- - // | | |-- ackFilterKey: - // | | |-- settleFailFilterKey: - // | | |-- fwdFilterKey: - // | | | - // | | |-- addBucketKey - // | | | |-- : - // | | | |-- : - // | | | ... - // | | | - // | | |-- failSettleBucketKey - // | | |-- : - // | | |-- : - // | | ... - // | | - // | |-- - // | | | - // | ... ... - // | - // | - // |-- - // | | - // | ... - // ... - // - fwdPackagesKey = []byte("fwd-packages") + // NewSwitchPackager instantiates a new SwitchPackager. + NewSwitchPackager = cstate.NewSwitchPackager - // addBucketKey is the bucket to which all Add log updates are written. - addBucketKey = []byte("add-updates") - - // failSettleBucketKey is the bucket to which all Settle/Fail log - // updates are written. - failSettleBucketKey = []byte("fail-settle-updates") - - // fwdFilterKey is a key used to write the set of Adds that passed - // validation and are to be forwarded to the switch. - // NOTE: The presence of this key within a forwarding package indicates - // that the package has reached FwdStateProcessed. - fwdFilterKey = []byte("fwd-filter-key") - - // ackFilterKey is a key used to access the PkgFilter indicating which - // Adds have received a Settle/Fail. This response may come from a - // number of sources, including: exitHop settle/fails, switch failures, - // chain arbiter interjections, as well as settle/fails from the - // next hop in the route. - ackFilterKey = []byte("ack-filter-key") - - // settleFailFilterKey is a key used to access the PkgFilter indicating - // which Settles/Fails in have been received and processed by the link - // that originally received the Add. - settleFailFilterKey = []byte("settle-fail-filter-key") + // NewChannelPackager creates a new packager for a single channel. + NewChannelPackager = cstate.NewChannelPackager ) - -// SettleFailAcker is a generic interface providing the ability to acknowledge -// settle/fail HTLCs stored in forwarding packages. -type SettleFailAcker interface { - // AckSettleFails atomically updates the settle-fail filters in *other* - // channels' forwarding packages. - AckSettleFails(tx kvdb.RwTx, settleFailRefs ...SettleFailRef) error -} - -// GlobalFwdPkgReader is an interface used to retrieve the forwarding packages -// of any active channel. -type GlobalFwdPkgReader interface { - // LoadChannelFwdPkgs loads all known forwarding packages for the given - // channel. - LoadChannelFwdPkgs(tx kvdb.RTx, - source lnwire.ShortChannelID) ([]*FwdPkg, error) -} - -// FwdOperator defines the interfaces for managing forwarding packages that are -// external to a particular channel. This interface is used by the switch to -// read forwarding packages from arbitrary channels, and acknowledge settles and -// fails for locally-sourced payments. -type FwdOperator interface { - // GlobalFwdPkgReader provides read access to all known forwarding - // packages - GlobalFwdPkgReader - - // SettleFailAcker grants the ability to acknowledge settles or fails - // residing in arbitrary forwarding packages. - SettleFailAcker -} - -// SwitchPackager is a concrete implementation of the FwdOperator interface. -// A SwitchPackager offers the ability to read any forwarding package, and ack -// arbitrary settle and fail HTLCs. -type SwitchPackager struct{} - -// NewSwitchPackager instantiates a new SwitchPackager. -func NewSwitchPackager() *SwitchPackager { - return &SwitchPackager{} -} - -// AckSettleFails atomically updates the settle-fail filters in *other* -// channels' forwarding packages, to mark that the switch has received a settle -// or fail residing in the forwarding package of a link. -func (*SwitchPackager) AckSettleFails(tx kvdb.RwTx, - settleFailRefs ...SettleFailRef) error { - - return ackSettleFails(tx, settleFailRefs) -} - -// LoadChannelFwdPkgs loads all forwarding packages for a particular channel. -func (*SwitchPackager) LoadChannelFwdPkgs(tx kvdb.RTx, - source lnwire.ShortChannelID) ([]*FwdPkg, error) { - - return loadChannelFwdPkgs(tx, source) -} - -// FwdPackager supports all operations required to modify fwd packages, such as -// creation, updates, reading, and removal. The interfaces are broken down in -// this way to support future delegation of the subinterfaces. -// -// TODO(ziggie): This kvdb transaction-level interface can likely be removed -// now that chanstate.OpenChannelFwdPkgStore provides the backend-independent -// forwarding package abstraction. -type FwdPackager interface { - // AddFwdPkg serializes and writes a FwdPkg for this channel at the - // remote commitment height included in the forwarding package. - AddFwdPkg(tx kvdb.RwTx, fwdPkg *FwdPkg) error - - // SetFwdFilter looks up the forwarding package at the remote `height` - // and sets the `fwdFilter`, marking the Adds for which: - // 1) We are not the exit node - // 2) Passed all validation - // 3) Should be forwarded to the switch immediately after a failure - SetFwdFilter(tx kvdb.RwTx, height uint64, fwdFilter *PkgFilter) error - - // AckAddHtlcs atomically updates the add filters in this channel's - // forwarding packages to mark the resolution of an Add that was - // received from the remote party. - AckAddHtlcs(tx kvdb.RwTx, addRefs ...AddRef) error - - // SettleFailAcker allows a link to acknowledge settle/fail HTLCs - // belonging to other channels. - SettleFailAcker - - // LoadFwdPkgs loads all known forwarding packages owned by this - // channel. - LoadFwdPkgs(tx kvdb.RTx) ([]*FwdPkg, error) - - // RemovePkg deletes a forwarding package owned by this channel at - // the provided remote `height`. - RemovePkg(tx kvdb.RwTx, height uint64) error - - // Wipe deletes all the forwarding packages owned by this channel. - Wipe(tx kvdb.RwTx) error -} - -// ChannelPackager is used by a channel to manage the lifecycle of its forwarding -// packages. The packager is tied to a particular source channel ID, allowing it -// to create and edit its own packages. Each packager also has the ability to -// remove fail/settle htlcs that correspond to an add contained in one of -// source's packages. -type ChannelPackager struct { - source lnwire.ShortChannelID -} - -// NewChannelPackager creates a new packager for a single channel. -func NewChannelPackager(source lnwire.ShortChannelID) *ChannelPackager { - return &ChannelPackager{ - source: source, - } -} - -// AddFwdPkg writes a newly locked in forwarding package to disk. -func (*ChannelPackager) AddFwdPkg(tx kvdb.RwTx, fwdPkg *FwdPkg) error { // nolint: dupl - fwdPkgBkt, err := tx.CreateTopLevelBucket(fwdPackagesKey) - if err != nil { - return err - } - - source := makeLogKey(fwdPkg.Source.ToUint64()) - sourceBkt, err := fwdPkgBkt.CreateBucketIfNotExists(source[:]) - if err != nil { - return err - } - - heightKey := makeLogKey(fwdPkg.Height) - heightBkt, err := sourceBkt.CreateBucketIfNotExists(heightKey[:]) - if err != nil { - return err - } - - // Write ADD updates we received at this commit height. - addBkt, err := heightBkt.CreateBucketIfNotExists(addBucketKey) - if err != nil { - return err - } - - // Write SETTLE/FAIL updates we received at this commit height. - failSettleBkt, err := heightBkt.CreateBucketIfNotExists(failSettleBucketKey) - if err != nil { - return err - } - - for i := range fwdPkg.Adds { - err = putLogUpdate(addBkt, uint16(i), &fwdPkg.Adds[i]) - if err != nil { - return err - } - } - - // Persist the initialized pkg filter, which will be used to determine - // when we can remove this forwarding package from disk. - var ackFilterBuf bytes.Buffer - if err := fwdPkg.AckFilter.Encode(&ackFilterBuf); err != nil { - return err - } - - if err := heightBkt.Put(ackFilterKey, ackFilterBuf.Bytes()); err != nil { - return err - } - - for i := range fwdPkg.SettleFails { - err = putLogUpdate(failSettleBkt, uint16(i), &fwdPkg.SettleFails[i]) - if err != nil { - return err - } - } - - var settleFailFilterBuf bytes.Buffer - err = fwdPkg.SettleFailFilter.Encode(&settleFailFilterBuf) - if err != nil { - return err - } - - return heightBkt.Put(settleFailFilterKey, settleFailFilterBuf.Bytes()) -} - -// putLogUpdate writes an htlc to the provided `bkt`, using `index` as the key. -func putLogUpdate(bkt kvdb.RwBucket, idx uint16, htlc *LogUpdate) error { - var b bytes.Buffer - if err := serializeLogUpdate(&b, htlc); err != nil { - return err - } - - return bkt.Put(uint16Key(idx), b.Bytes()) -} - -// LoadFwdPkgs scans the forwarding log for any packages that haven't been -// processed, and returns their deserialized log updates in a map indexed by the -// remote commitment height at which the updates were locked in. -func (p *ChannelPackager) LoadFwdPkgs(tx kvdb.RTx) ([]*FwdPkg, error) { - return loadChannelFwdPkgs(tx, p.source) -} - -// loadChannelFwdPkgs loads all forwarding packages owned by `source`. -func loadChannelFwdPkgs(tx kvdb.RTx, source lnwire.ShortChannelID) ([]*FwdPkg, error) { - fwdPkgBkt := tx.ReadBucket(fwdPackagesKey) - if fwdPkgBkt == nil { - return nil, nil - } - - sourceKey := makeLogKey(source.ToUint64()) - sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) - if sourceBkt == nil { - return nil, nil - } - - var heights []uint64 - if err := sourceBkt.ForEach(func(k, _ []byte) error { - if len(k) != 8 { - return ErrCorruptedFwdPkg - } - - heights = append(heights, byteOrder.Uint64(k)) - - return nil - }); err != nil { - return nil, err - } - - // Load the forwarding package for each retrieved height. - fwdPkgs := make([]*FwdPkg, 0, len(heights)) - for _, height := range heights { - fwdPkg, err := loadFwdPkg(fwdPkgBkt, source, height) - if err != nil { - return nil, err - } - - fwdPkgs = append(fwdPkgs, fwdPkg) - } - - return fwdPkgs, nil -} - -// loadFwdPkg reads the packager's fwd pkg at a given height, and determines the -// appropriate FwdState. -func loadFwdPkg(fwdPkgBkt kvdb.RBucket, source lnwire.ShortChannelID, - height uint64) (*FwdPkg, error) { - - sourceKey := makeLogKey(source.ToUint64()) - sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) - if sourceBkt == nil { - return nil, ErrCorruptedFwdPkg - } - - heightKey := makeLogKey(height) - heightBkt := sourceBkt.NestedReadBucket(heightKey[:]) - if heightBkt == nil { - return nil, ErrCorruptedFwdPkg - } - - // Load ADDs from disk. - addBkt := heightBkt.NestedReadBucket(addBucketKey) - if addBkt == nil { - return nil, ErrCorruptedFwdPkg - } - - adds, err := loadHtlcs(addBkt) - if err != nil { - return nil, err - } - - // Load ack filter from disk. - ackFilterBytes := heightBkt.Get(ackFilterKey) - if ackFilterBytes == nil { - return nil, ErrCorruptedFwdPkg - } - ackFilterReader := bytes.NewReader(ackFilterBytes) - - ackFilter := &PkgFilter{} - if err := ackFilter.Decode(ackFilterReader); err != nil { - return nil, err - } - - // Load SETTLE/FAILs from disk. - failSettleBkt := heightBkt.NestedReadBucket(failSettleBucketKey) - if failSettleBkt == nil { - return nil, ErrCorruptedFwdPkg - } - - failSettles, err := loadHtlcs(failSettleBkt) - if err != nil { - return nil, err - } - - // Load settle fail filter from disk. - settleFailFilterBytes := heightBkt.Get(settleFailFilterKey) - if settleFailFilterBytes == nil { - return nil, ErrCorruptedFwdPkg - } - settleFailFilterReader := bytes.NewReader(settleFailFilterBytes) - - settleFailFilter := &PkgFilter{} - if err := settleFailFilter.Decode(settleFailFilterReader); err != nil { - return nil, err - } - - // Initialize the fwding package, which always starts in the - // FwdStateLockedIn. We can determine what state the package was left in - // by examining constraints on the information loaded from disk. - fwdPkg := &FwdPkg{ - Source: source, - State: FwdStateLockedIn, - Height: height, - Adds: adds, - AckFilter: ackFilter, - SettleFails: failSettles, - SettleFailFilter: settleFailFilter, - } - - // Check if the forward filter has been persisted to disk. - // This indicates whether the Adds in this package have been processed. - // - // NOTE: We also expect packages with no Adds (settle/fail only packages - // or empty packages) to have the fwd filter set to signal that the - // packages have been processed. - fwdFilterBytes := heightBkt.Get(fwdFilterKey) - - // Handle packages with Adds that haven't been processed yet. - if fwdFilterBytes == nil { - // Create a new forward filter for the unprocessed Adds. - nAdds := uint16(len(adds)) - fwdPkg.FwdFilter = NewPkgFilter(nAdds) - - return fwdPkg, nil - } - - // Load the existing forward filter from disk. - fwdFilterReader := bytes.NewReader(fwdFilterBytes) - fwdPkg.FwdFilter = &PkgFilter{} - if err := fwdPkg.FwdFilter.Decode(fwdFilterReader); err != nil { - return nil, err - } - - // Mark the package as processed since the forward filter exists. - fwdPkg.State = FwdStateProcessed - - // If every add, settle, and fail has been fully acknowledged, we can - // safely set the package's state to FwdStateCompleted, signalling that - // it can be garbage collected. - if fwdPkg.AckFilter.IsFull() && fwdPkg.SettleFailFilter.IsFull() { - fwdPkg.State = FwdStateCompleted - } - - return fwdPkg, nil -} - -// loadHtlcs retrieves all serialized htlcs in a bucket, returning -// them in order of the indexes they were written under. -func loadHtlcs(bkt kvdb.RBucket) ([]LogUpdate, error) { - var htlcs []LogUpdate - if err := bkt.ForEach(func(_, v []byte) error { - htlc, err := deserializeLogUpdate(bytes.NewReader(v)) - if err != nil { - return err - } - - htlcs = append(htlcs, *htlc) - - return nil - }); err != nil { - return nil, err - } - - return htlcs, nil -} - -// SetFwdFilter writes the set of indexes corresponding to Adds at the -// `height` that are to be forwarded to the switch. Calling this method causes -// the forwarding package at `height` to be in FwdStateProcessed. We write this -// forwarding decision so that we always arrive at the same behavior for HTLCs -// leaving this channel. After a restart, we skip validation of these Adds, -// since they are assumed to have already been validated, and make the switch or -// outgoing link responsible for handling replays. -func (p *ChannelPackager) SetFwdFilter(tx kvdb.RwTx, height uint64, - fwdFilter *PkgFilter) error { - - fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) - if fwdPkgBkt == nil { - return ErrCorruptedFwdPkg - } - - source := makeLogKey(p.source.ToUint64()) - sourceBkt := fwdPkgBkt.NestedReadWriteBucket(source[:]) - if sourceBkt == nil { - return ErrCorruptedFwdPkg - } - - heightKey := makeLogKey(height) - heightBkt := sourceBkt.NestedReadWriteBucket(heightKey[:]) - if heightBkt == nil { - return ErrCorruptedFwdPkg - } - - // If the fwd filter has already been written, we return early to avoid - // modifying the persistent state. - forwardedAddsBytes := heightBkt.Get(fwdFilterKey) - if forwardedAddsBytes != nil { - return nil - } - - // Otherwise we serialize and write the provided fwd filter. - var b bytes.Buffer - if err := fwdFilter.Encode(&b); err != nil { - return err - } - - return heightBkt.Put(fwdFilterKey, b.Bytes()) -} - -// AckAddHtlcs accepts a list of references to add htlcs, and updates the -// AckAddFilter of those forwarding packages to indicate that a settle or fail -// has been received in response to the add. -func (p *ChannelPackager) AckAddHtlcs(tx kvdb.RwTx, addRefs ...AddRef) error { - if len(addRefs) == 0 { - return nil - } - - fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) - if fwdPkgBkt == nil { - return ErrCorruptedFwdPkg - } - - sourceKey := makeLogKey(p.source.ToUint64()) - sourceBkt := fwdPkgBkt.NestedReadWriteBucket(sourceKey[:]) - if sourceBkt == nil { - return ErrCorruptedFwdPkg - } - - // Organize the forward references such that we just get a single slice - // of indexes for each unique height. - heightDiffs := make(map[uint64][]uint16) - for _, addRef := range addRefs { - heightDiffs[addRef.Height] = append( - heightDiffs[addRef.Height], - addRef.Index, - ) - } - - // Load each height bucket once and remove all acked htlcs at that - // height. - for height, indexes := range heightDiffs { - err := ackAddHtlcsAtHeight(sourceBkt, height, indexes) - if err != nil { - return err - } - } - - return nil -} - -// ackAddHtlcsAtHeight updates the AddAckFilter of a single forwarding package -// with a list of indexes, writing the resulting filter back in its place. -func ackAddHtlcsAtHeight(sourceBkt kvdb.RwBucket, height uint64, - indexes []uint16) error { - - heightKey := makeLogKey(height) - heightBkt := sourceBkt.NestedReadWriteBucket(heightKey[:]) - if heightBkt == nil { - // If the height bucket isn't found, this could be because the - // forwarding package was already removed. We'll return nil to - // signal that the operation is successful, as there is nothing - // to ack. - return nil - } - - // Load ack filter from disk. - ackFilterBytes := heightBkt.Get(ackFilterKey) - if ackFilterBytes == nil { - return ErrCorruptedFwdPkg - } - - ackFilter := &PkgFilter{} - ackFilterReader := bytes.NewReader(ackFilterBytes) - if err := ackFilter.Decode(ackFilterReader); err != nil { - return err - } - - // Update the ack filter for this height. - for _, index := range indexes { - ackFilter.Set(index) - } - - // Write the resulting filter to disk. - var ackFilterBuf bytes.Buffer - if err := ackFilter.Encode(&ackFilterBuf); err != nil { - return err - } - - return heightBkt.Put(ackFilterKey, ackFilterBuf.Bytes()) -} - -// AckSettleFails persistently acknowledges settles or fails from a remote forwarding -// package. This should only be called after the source of the Add has locked in -// the settle/fail, or it becomes otherwise safe to forgo retransmitting the -// settle/fail after a restart. -func (p *ChannelPackager) AckSettleFails(tx kvdb.RwTx, settleFailRefs ...SettleFailRef) error { - return ackSettleFails(tx, settleFailRefs) -} - -// ackSettleFails persistently acknowledges a batch of settle fail references. -func ackSettleFails(tx kvdb.RwTx, settleFailRefs []SettleFailRef) error { - if len(settleFailRefs) == 0 { - return nil - } - - fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) - if fwdPkgBkt == nil { - return ErrCorruptedFwdPkg - } - - // Organize the forward references such that we just get a single slice - // of indexes for each unique destination-height pair. - destHeightDiffs := make(map[lnwire.ShortChannelID]map[uint64][]uint16) - for _, settleFailRef := range settleFailRefs { - destHeights, ok := destHeightDiffs[settleFailRef.Source] - if !ok { - destHeights = make(map[uint64][]uint16) - destHeightDiffs[settleFailRef.Source] = destHeights - } - - destHeights[settleFailRef.Height] = append( - destHeights[settleFailRef.Height], - settleFailRef.Index, - ) - } - - // With the references organized by destination and height, we now load - // each remote bucket, and update the settle fail filter for any - // settle/fail htlcs. - for dest, destHeights := range destHeightDiffs { - destKey := makeLogKey(dest.ToUint64()) - destBkt := fwdPkgBkt.NestedReadWriteBucket(destKey[:]) - if destBkt == nil { - // If the destination bucket is not found, this is - // likely the result of the destination channel being - // closed and having it's forwarding packages wiped. We - // won't treat this as an error, because the response - // will no longer be retransmitted internally. - continue - } - - for height, indexes := range destHeights { - err := ackSettleFailsAtHeight(destBkt, height, indexes) - if err != nil { - return err - } - } - } - - return nil -} - -// ackSettleFailsAtHeight given a destination bucket, acks the provided indexes -// at particular a height by updating the settle fail filter. -func ackSettleFailsAtHeight(destBkt kvdb.RwBucket, height uint64, - indexes []uint16) error { - - heightKey := makeLogKey(height) - heightBkt := destBkt.NestedReadWriteBucket(heightKey[:]) - if heightBkt == nil { - // If the height bucket isn't found, this could be because the - // forwarding package was already removed. We'll return nil to - // signal that the operation is as there is nothing to ack. - return nil - } - - // Load ack filter from disk. - settleFailFilterBytes := heightBkt.Get(settleFailFilterKey) - if settleFailFilterBytes == nil { - return ErrCorruptedFwdPkg - } - - settleFailFilter := &PkgFilter{} - settleFailFilterReader := bytes.NewReader(settleFailFilterBytes) - if err := settleFailFilter.Decode(settleFailFilterReader); err != nil { - return err - } - - // Update the ack filter for this height. - for _, index := range indexes { - settleFailFilter.Set(index) - } - - // Write the resulting filter to disk. - var settleFailFilterBuf bytes.Buffer - if err := settleFailFilter.Encode(&settleFailFilterBuf); err != nil { - return err - } - - return heightBkt.Put(settleFailFilterKey, settleFailFilterBuf.Bytes()) -} - -// RemovePkg deletes the forwarding package at the given height from the -// packager's source bucket. -func (p *ChannelPackager) RemovePkg(tx kvdb.RwTx, height uint64) error { - fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) - if fwdPkgBkt == nil { - return nil - } - - sourceBytes := makeLogKey(p.source.ToUint64()) - sourceBkt := fwdPkgBkt.NestedReadWriteBucket(sourceBytes[:]) - if sourceBkt == nil { - return ErrCorruptedFwdPkg - } - - heightKey := makeLogKey(height) - - return sourceBkt.DeleteNestedBucket(heightKey[:]) -} - -// Wipe deletes all the channel's forwarding packages, if any. -func (p *ChannelPackager) Wipe(tx kvdb.RwTx) error { - // If the root bucket doesn't exist, there's no need to delete. - fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) - if fwdPkgBkt == nil { - return nil - } - - sourceBytes := makeLogKey(p.source.ToUint64()) - - // If the nested bucket doesn't exist, there's no need to delete. - if fwdPkgBkt.NestedReadWriteBucket(sourceBytes[:]) == nil { - return nil - } - - return fwdPkgBkt.DeleteNestedBucket(sourceBytes[:]) -} - -// uint16Key writes the provided 16-bit unsigned integer to a 2-byte slice. -func uint16Key(i uint16) []byte { - key := make([]byte, 2) - byteOrder.PutUint16(key, i) - return key -} - -// Compile-time constraint to ensure that ChannelPackager implements the public -// FwdPackager interface. -var _ FwdPackager = (*ChannelPackager)(nil) - -// Compile-time constraint to ensure that SwitchPackager implements the public -// FwdOperator interface. -var _ FwdOperator = (*SwitchPackager)(nil) diff --git a/chanstate/kv_forwarding_package.go b/chanstate/kv_forwarding_package.go new file mode 100644 index 0000000000..c740ebc10c --- /dev/null +++ b/chanstate/kv_forwarding_package.go @@ -0,0 +1,747 @@ +package chanstate + +import ( + "bytes" + "errors" + + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" +) + +//nolint:ll +var ( + // ErrCorruptedFwdPkg signals that the on-disk structure of the + // forwarding package has potentially been mangled. + ErrCorruptedFwdPkg = errors.New("fwding package db has been corrupted") + + // fwdPackagesKey is the root-level bucket that all forwarding packages + // are written. This bucket is further subdivided based on the short + // channel ID of each channel. + // + // Bucket hierarchy: + // + // fwdPackagesKey(root-bucket) + // | + // |-- + // | | + // | |-- + // | | |-- ackFilterKey: + // | | |-- settleFailFilterKey: + // | | |-- fwdFilterKey: + // | | | + // | | |-- addBucketKey + // | | | |-- : + // | | | |-- : + // | | | ... + // | | | + // | | |-- failSettleBucketKey + // | | |-- : + // | | |-- : + // | | ... + // | | + // | |-- + // | | | + // | ... ... + // | + // | + // |-- + // | | + // | ... + // ... + // + fwdPackagesKey = []byte("fwd-packages") + + // addBucketKey is the bucket to which all Add log updates are written. + addBucketKey = []byte("add-updates") + + // failSettleBucketKey is the bucket to which all Settle/Fail log + // updates are written. + failSettleBucketKey = []byte("fail-settle-updates") + + // fwdFilterKey is a key used to write the set of Adds that passed + // validation and are to be forwarded to the switch. + // NOTE: The presence of this key within a forwarding package indicates + // that the package has reached FwdStateProcessed. + fwdFilterKey = []byte("fwd-filter-key") + + // ackFilterKey is a key used to access the PkgFilter indicating which + // Adds have received a Settle/Fail. This response may come from a + // number of sources, including: exitHop settle/fails, switch failures, + // chain arbiter interjections, as well as settle/fails from the + // next hop in the route. + ackFilterKey = []byte("ack-filter-key") + + // settleFailFilterKey is a key used to access the PkgFilter indicating + // which Settles/Fails in have been received and processed by the link + // that originally received the Add. + settleFailFilterKey = []byte("settle-fail-filter-key") +) + +// FwdPackagesBucketKey returns the root-level bucket key that stores +// forwarding packages. +func FwdPackagesBucketKey() []byte { + return fwdPackagesKey +} + +// SettleFailAcker is a generic interface providing the ability to acknowledge +// settle/fail HTLCs stored in forwarding packages. +type SettleFailAcker interface { + // AckSettleFails atomically updates the settle-fail filters in *other* + // channels' forwarding packages. + AckSettleFails(tx kvdb.RwTx, settleFailRefs ...SettleFailRef) error +} + +// GlobalFwdPkgReader is an interface used to retrieve the forwarding packages +// of any active channel. +type GlobalFwdPkgReader interface { + // LoadChannelFwdPkgs loads all known forwarding packages for the given + // channel. + LoadChannelFwdPkgs(tx kvdb.RTx, + source lnwire.ShortChannelID) ([]*FwdPkg, error) +} + +// FwdOperator defines the interfaces for managing forwarding packages that are +// external to a particular channel. This interface is used by the switch to +// read forwarding packages from arbitrary channels, and acknowledge settles and +// fails for locally-sourced payments. +type FwdOperator interface { + // GlobalFwdPkgReader provides read access to all known forwarding + // packages + GlobalFwdPkgReader + + // SettleFailAcker grants the ability to acknowledge settles or fails + // residing in arbitrary forwarding packages. + SettleFailAcker +} + +// SwitchPackager is a concrete implementation of the FwdOperator interface. +// A SwitchPackager offers the ability to read any forwarding package, and ack +// arbitrary settle and fail HTLCs. +type SwitchPackager struct{} + +// NewSwitchPackager instantiates a new SwitchPackager. +func NewSwitchPackager() *SwitchPackager { + return &SwitchPackager{} +} + +// AckSettleFails atomically updates the settle-fail filters in *other* +// channels' forwarding packages, to mark that the switch has received a settle +// or fail residing in the forwarding package of a link. +func (*SwitchPackager) AckSettleFails(tx kvdb.RwTx, + settleFailRefs ...SettleFailRef) error { + + return ackSettleFails(tx, settleFailRefs) +} + +// LoadChannelFwdPkgs loads all forwarding packages for a particular channel. +func (*SwitchPackager) LoadChannelFwdPkgs(tx kvdb.RTx, + source lnwire.ShortChannelID) ([]*FwdPkg, error) { + + return loadChannelFwdPkgs(tx, source) +} + +// FwdPackager supports all operations required to modify fwd packages, such as +// creation, updates, reading, and removal. The interfaces are broken down in +// this way to support future delegation of the subinterfaces. +type FwdPackager interface { + // AddFwdPkg serializes and writes a FwdPkg for this channel at the + // remote commitment height included in the forwarding package. + AddFwdPkg(tx kvdb.RwTx, fwdPkg *FwdPkg) error + + // SetFwdFilter looks up the forwarding package at the remote `height` + // and sets the `fwdFilter`, marking the Adds for which: + // 1) We are not the exit node + // 2) Passed all validation + // 3) Should be forwarded to the switch immediately after a failure + SetFwdFilter(tx kvdb.RwTx, height uint64, fwdFilter *PkgFilter) error + + // AckAddHtlcs atomically updates the add filters in this channel's + // forwarding packages to mark the resolution of an Add that was + // received from the remote party. + AckAddHtlcs(tx kvdb.RwTx, addRefs ...AddRef) error + + // SettleFailAcker allows a link to acknowledge settle/fail HTLCs + // belonging to other channels. + SettleFailAcker + + // LoadFwdPkgs loads all known forwarding packages owned by this + // channel. + LoadFwdPkgs(tx kvdb.RTx) ([]*FwdPkg, error) + + // RemovePkg deletes a forwarding package owned by this channel at + // the provided remote `height`. + RemovePkg(tx kvdb.RwTx, height uint64) error + + // Wipe deletes all the forwarding packages owned by this channel. + Wipe(tx kvdb.RwTx) error +} + +// ChannelPackager is used by a channel to manage the lifecycle of its +// forwarding packages. The packager is tied to a particular source channel ID, +// allowing it to create and edit its own packages. Each packager also has the +// ability to +// remove fail/settle htlcs that correspond to an add contained in one of +// source's packages. +type ChannelPackager struct { + source lnwire.ShortChannelID +} + +// NewChannelPackager creates a new packager for a single channel. +func NewChannelPackager(source lnwire.ShortChannelID) *ChannelPackager { + return &ChannelPackager{ + source: source, + } +} + +// Source returns the short channel ID of the channel this packager manages +// forwarding packages for. +// +// NOTE: This accessor is only needed because TestRefresh still lives in the +// channeldb package and can no longer reach the unexported source field +// directly. It can be removed once all channel state tests have been moved +// into the chanstate package. +func (p *ChannelPackager) Source() lnwire.ShortChannelID { + return p.source +} + +// AddFwdPkg writes a newly locked in forwarding package to disk. +func (*ChannelPackager) AddFwdPkg(tx kvdb.RwTx, fwdPkg *FwdPkg) error { + fwdPkgBkt, err := tx.CreateTopLevelBucket(fwdPackagesKey) + if err != nil { + return err + } + + source := makeLogKey(fwdPkg.Source.ToUint64()) + sourceBkt, err := fwdPkgBkt.CreateBucketIfNotExists(source[:]) + if err != nil { + return err + } + + heightKey := makeLogKey(fwdPkg.Height) + heightBkt, err := sourceBkt.CreateBucketIfNotExists(heightKey[:]) + if err != nil { + return err + } + + // Write ADD updates we received at this commit height. + addBkt, err := heightBkt.CreateBucketIfNotExists(addBucketKey) + if err != nil { + return err + } + + // Write SETTLE/FAIL updates we received at this commit height. + failSettleBkt, err := heightBkt.CreateBucketIfNotExists( + failSettleBucketKey, + ) + if err != nil { + return err + } + + for i := range fwdPkg.Adds { + err = putLogUpdate(addBkt, uint16(i), &fwdPkg.Adds[i]) + if err != nil { + return err + } + } + + // Persist the initialized pkg filter, which will be used to determine + // when we can remove this forwarding package from disk. + var ackFilterBuf bytes.Buffer + if err := fwdPkg.AckFilter.Encode(&ackFilterBuf); err != nil { + return err + } + + err = heightBkt.Put(ackFilterKey, ackFilterBuf.Bytes()) + if err != nil { + return err + } + + for i := range fwdPkg.SettleFails { + err = putLogUpdate( + failSettleBkt, uint16(i), &fwdPkg.SettleFails[i], + ) + if err != nil { + return err + } + } + + var settleFailFilterBuf bytes.Buffer + err = fwdPkg.SettleFailFilter.Encode(&settleFailFilterBuf) + if err != nil { + return err + } + + return heightBkt.Put(settleFailFilterKey, settleFailFilterBuf.Bytes()) +} + +// putLogUpdate writes an htlc to the provided `bkt`, using `index` as the key. +func putLogUpdate(bkt kvdb.RwBucket, idx uint16, htlc *LogUpdate) error { + var b bytes.Buffer + if err := serializeLogUpdate(&b, htlc); err != nil { + return err + } + + return bkt.Put(uint16Key(idx), b.Bytes()) +} + +// LoadFwdPkgs scans the forwarding log for any packages that haven't been +// processed, and returns their deserialized log updates in a map indexed by the +// remote commitment height at which the updates were locked in. +func (p *ChannelPackager) LoadFwdPkgs(tx kvdb.RTx) ([]*FwdPkg, error) { + return loadChannelFwdPkgs(tx, p.source) +} + +// loadChannelFwdPkgs loads all forwarding packages owned by `source`. +func loadChannelFwdPkgs(tx kvdb.RTx, + source lnwire.ShortChannelID) ([]*FwdPkg, error) { + + fwdPkgBkt := tx.ReadBucket(fwdPackagesKey) + if fwdPkgBkt == nil { + return nil, nil + } + + sourceKey := makeLogKey(source.ToUint64()) + sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) + if sourceBkt == nil { + return nil, nil + } + + var heights []uint64 + if err := sourceBkt.ForEach(func(k, _ []byte) error { + if len(k) != 8 { + return ErrCorruptedFwdPkg + } + + heights = append(heights, byteOrder.Uint64(k)) + + return nil + }); err != nil { + return nil, err + } + + // Load the forwarding package for each retrieved height. + fwdPkgs := make([]*FwdPkg, 0, len(heights)) + for _, height := range heights { + fwdPkg, err := loadFwdPkg(fwdPkgBkt, source, height) + if err != nil { + return nil, err + } + + fwdPkgs = append(fwdPkgs, fwdPkg) + } + + return fwdPkgs, nil +} + +// loadFwdPkg reads the packager's fwd pkg at a given height, and determines the +// appropriate FwdState. +func loadFwdPkg(fwdPkgBkt kvdb.RBucket, source lnwire.ShortChannelID, + height uint64) (*FwdPkg, error) { + + sourceKey := makeLogKey(source.ToUint64()) + sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) + if sourceBkt == nil { + return nil, ErrCorruptedFwdPkg + } + + heightKey := makeLogKey(height) + heightBkt := sourceBkt.NestedReadBucket(heightKey[:]) + if heightBkt == nil { + return nil, ErrCorruptedFwdPkg + } + + // Load ADDs from disk. + addBkt := heightBkt.NestedReadBucket(addBucketKey) + if addBkt == nil { + return nil, ErrCorruptedFwdPkg + } + + adds, err := loadHtlcs(addBkt) + if err != nil { + return nil, err + } + + // Load ack filter from disk. + ackFilterBytes := heightBkt.Get(ackFilterKey) + if ackFilterBytes == nil { + return nil, ErrCorruptedFwdPkg + } + ackFilterReader := bytes.NewReader(ackFilterBytes) + + ackFilter := &PkgFilter{} + if err := ackFilter.Decode(ackFilterReader); err != nil { + return nil, err + } + + // Load SETTLE/FAILs from disk. + failSettleBkt := heightBkt.NestedReadBucket(failSettleBucketKey) + if failSettleBkt == nil { + return nil, ErrCorruptedFwdPkg + } + + failSettles, err := loadHtlcs(failSettleBkt) + if err != nil { + return nil, err + } + + // Load settle fail filter from disk. + settleFailFilterBytes := heightBkt.Get(settleFailFilterKey) + if settleFailFilterBytes == nil { + return nil, ErrCorruptedFwdPkg + } + settleFailFilterReader := bytes.NewReader(settleFailFilterBytes) + + settleFailFilter := &PkgFilter{} + if err := settleFailFilter.Decode(settleFailFilterReader); err != nil { + return nil, err + } + + // Initialize the fwding package, which always starts in the + // FwdStateLockedIn. We can determine what state the package was left in + // by examining constraints on the information loaded from disk. + fwdPkg := &FwdPkg{ + Source: source, + State: FwdStateLockedIn, + Height: height, + Adds: adds, + AckFilter: ackFilter, + SettleFails: failSettles, + SettleFailFilter: settleFailFilter, + } + + // Check if the forward filter has been persisted to disk. + // This indicates whether the Adds in this package have been processed. + // + // NOTE: We also expect packages with no Adds (settle/fail only packages + // or empty packages) to have the fwd filter set to signal that the + // packages have been processed. + fwdFilterBytes := heightBkt.Get(fwdFilterKey) + + // Handle packages with Adds that haven't been processed yet. + if fwdFilterBytes == nil { + // Create a new forward filter for the unprocessed Adds. + nAdds := uint16(len(adds)) + fwdPkg.FwdFilter = NewPkgFilter(nAdds) + + return fwdPkg, nil + } + + // Load the existing forward filter from disk. + fwdFilterReader := bytes.NewReader(fwdFilterBytes) + fwdPkg.FwdFilter = &PkgFilter{} + if err := fwdPkg.FwdFilter.Decode(fwdFilterReader); err != nil { + return nil, err + } + + // Mark the package as processed since the forward filter exists. + fwdPkg.State = FwdStateProcessed + + // If every add, settle, and fail has been fully acknowledged, we can + // safely set the package's state to FwdStateCompleted, signalling that + // it can be garbage collected. + if fwdPkg.AckFilter.IsFull() && fwdPkg.SettleFailFilter.IsFull() { + fwdPkg.State = FwdStateCompleted + } + + return fwdPkg, nil +} + +// loadHtlcs retrieves all serialized htlcs in a bucket, returning +// them in order of the indexes they were written under. +func loadHtlcs(bkt kvdb.RBucket) ([]LogUpdate, error) { + var htlcs []LogUpdate + if err := bkt.ForEach(func(_, v []byte) error { + htlc, err := deserializeLogUpdate(bytes.NewReader(v)) + if err != nil { + return err + } + + htlcs = append(htlcs, *htlc) + + return nil + }); err != nil { + return nil, err + } + + return htlcs, nil +} + +// SetFwdFilter writes the set of indexes corresponding to Adds at the +// `height` that are to be forwarded to the switch. Calling this method causes +// the forwarding package at `height` to be in FwdStateProcessed. We write this +// forwarding decision so that we always arrive at the same behavior for HTLCs +// leaving this channel. After a restart, we skip validation of these Adds, +// since they are assumed to have already been validated, and make the switch or +// outgoing link responsible for handling replays. +func (p *ChannelPackager) SetFwdFilter(tx kvdb.RwTx, height uint64, + fwdFilter *PkgFilter) error { + + fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) + if fwdPkgBkt == nil { + return ErrCorruptedFwdPkg + } + + source := makeLogKey(p.source.ToUint64()) + sourceBkt := fwdPkgBkt.NestedReadWriteBucket(source[:]) + if sourceBkt == nil { + return ErrCorruptedFwdPkg + } + + heightKey := makeLogKey(height) + heightBkt := sourceBkt.NestedReadWriteBucket(heightKey[:]) + if heightBkt == nil { + return ErrCorruptedFwdPkg + } + + // If the fwd filter has already been written, we return early to avoid + // modifying the persistent state. + forwardedAddsBytes := heightBkt.Get(fwdFilterKey) + if forwardedAddsBytes != nil { + return nil + } + + // Otherwise we serialize and write the provided fwd filter. + var b bytes.Buffer + if err := fwdFilter.Encode(&b); err != nil { + return err + } + + return heightBkt.Put(fwdFilterKey, b.Bytes()) +} + +// AckAddHtlcs accepts a list of references to add htlcs, and updates the +// AckAddFilter of those forwarding packages to indicate that a settle or fail +// has been received in response to the add. +func (p *ChannelPackager) AckAddHtlcs(tx kvdb.RwTx, addRefs ...AddRef) error { + if len(addRefs) == 0 { + return nil + } + + fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) + if fwdPkgBkt == nil { + return ErrCorruptedFwdPkg + } + + sourceKey := makeLogKey(p.source.ToUint64()) + sourceBkt := fwdPkgBkt.NestedReadWriteBucket(sourceKey[:]) + if sourceBkt == nil { + return ErrCorruptedFwdPkg + } + + // Organize the forward references such that we just get a single slice + // of indexes for each unique height. + heightDiffs := make(map[uint64][]uint16) + for _, addRef := range addRefs { + heightDiffs[addRef.Height] = append( + heightDiffs[addRef.Height], + addRef.Index, + ) + } + + // Load each height bucket once and remove all acked htlcs at that + // height. + for height, indexes := range heightDiffs { + err := ackAddHtlcsAtHeight(sourceBkt, height, indexes) + if err != nil { + return err + } + } + + return nil +} + +// ackAddHtlcsAtHeight updates the AddAckFilter of a single forwarding package +// with a list of indexes, writing the resulting filter back in its place. +func ackAddHtlcsAtHeight(sourceBkt kvdb.RwBucket, height uint64, + indexes []uint16) error { + + heightKey := makeLogKey(height) + heightBkt := sourceBkt.NestedReadWriteBucket(heightKey[:]) + if heightBkt == nil { + // If the height bucket isn't found, this could be because the + // forwarding package was already removed. We'll return nil to + // signal that the operation is successful, as there is nothing + // to ack. + return nil + } + + // Load ack filter from disk. + ackFilterBytes := heightBkt.Get(ackFilterKey) + if ackFilterBytes == nil { + return ErrCorruptedFwdPkg + } + + ackFilter := &PkgFilter{} + ackFilterReader := bytes.NewReader(ackFilterBytes) + if err := ackFilter.Decode(ackFilterReader); err != nil { + return err + } + + // Update the ack filter for this height. + for _, index := range indexes { + ackFilter.Set(index) + } + + // Write the resulting filter to disk. + var ackFilterBuf bytes.Buffer + if err := ackFilter.Encode(&ackFilterBuf); err != nil { + return err + } + + return heightBkt.Put(ackFilterKey, ackFilterBuf.Bytes()) +} + +// AckSettleFails persistently acknowledges settles or fails from a remote +// forwarding package. This should only be called after the source of the Add +// has locked in the settle/fail, or it becomes otherwise safe to forgo +// retransmitting the settle/fail after a restart. +func (p *ChannelPackager) AckSettleFails(tx kvdb.RwTx, + settleFailRefs ...SettleFailRef) error { + + return ackSettleFails(tx, settleFailRefs) +} + +// ackSettleFails persistently acknowledges a batch of settle fail references. +func ackSettleFails(tx kvdb.RwTx, settleFailRefs []SettleFailRef) error { + if len(settleFailRefs) == 0 { + return nil + } + + fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) + if fwdPkgBkt == nil { + return ErrCorruptedFwdPkg + } + + // Organize the forward references such that we just get a single slice + // of indexes for each unique destination-height pair. + destHeightDiffs := make(map[lnwire.ShortChannelID]map[uint64][]uint16) + for _, settleFailRef := range settleFailRefs { + destHeights, ok := destHeightDiffs[settleFailRef.Source] + if !ok { + destHeights = make(map[uint64][]uint16) + destHeightDiffs[settleFailRef.Source] = destHeights + } + + destHeights[settleFailRef.Height] = append( + destHeights[settleFailRef.Height], + settleFailRef.Index, + ) + } + + // With the references organized by destination and height, we now load + // each remote bucket, and update the settle fail filter for any + // settle/fail htlcs. + for dest, destHeights := range destHeightDiffs { + destKey := makeLogKey(dest.ToUint64()) + destBkt := fwdPkgBkt.NestedReadWriteBucket(destKey[:]) + if destBkt == nil { + // If the destination bucket is not found, this is + // likely the result of the destination channel being + // closed and having it's forwarding packages wiped. We + // won't treat this as an error, because the response + // will no longer be retransmitted internally. + continue + } + + for height, indexes := range destHeights { + err := ackSettleFailsAtHeight(destBkt, height, indexes) + if err != nil { + return err + } + } + } + + return nil +} + +// ackSettleFailsAtHeight given a destination bucket, acks the provided indexes +// at particular a height by updating the settle fail filter. +func ackSettleFailsAtHeight(destBkt kvdb.RwBucket, height uint64, + indexes []uint16) error { + + heightKey := makeLogKey(height) + heightBkt := destBkt.NestedReadWriteBucket(heightKey[:]) + if heightBkt == nil { + // If the height bucket isn't found, this could be because the + // forwarding package was already removed. We'll return nil to + // signal that the operation is as there is nothing to ack. + return nil + } + + // Load ack filter from disk. + settleFailFilterBytes := heightBkt.Get(settleFailFilterKey) + if settleFailFilterBytes == nil { + return ErrCorruptedFwdPkg + } + + settleFailFilter := &PkgFilter{} + settleFailFilterReader := bytes.NewReader(settleFailFilterBytes) + if err := settleFailFilter.Decode(settleFailFilterReader); err != nil { + return err + } + + // Update the ack filter for this height. + for _, index := range indexes { + settleFailFilter.Set(index) + } + + // Write the resulting filter to disk. + var settleFailFilterBuf bytes.Buffer + if err := settleFailFilter.Encode(&settleFailFilterBuf); err != nil { + return err + } + + return heightBkt.Put(settleFailFilterKey, settleFailFilterBuf.Bytes()) +} + +// RemovePkg deletes the forwarding package at the given height from the +// packager's source bucket. +func (p *ChannelPackager) RemovePkg(tx kvdb.RwTx, height uint64) error { + fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) + if fwdPkgBkt == nil { + return nil + } + + sourceBytes := makeLogKey(p.source.ToUint64()) + sourceBkt := fwdPkgBkt.NestedReadWriteBucket(sourceBytes[:]) + if sourceBkt == nil { + return ErrCorruptedFwdPkg + } + + heightKey := makeLogKey(height) + + return sourceBkt.DeleteNestedBucket(heightKey[:]) +} + +// Wipe deletes all the channel's forwarding packages, if any. +func (p *ChannelPackager) Wipe(tx kvdb.RwTx) error { + // If the root bucket doesn't exist, there's no need to delete. + fwdPkgBkt := tx.ReadWriteBucket(fwdPackagesKey) + if fwdPkgBkt == nil { + return nil + } + + sourceBytes := makeLogKey(p.source.ToUint64()) + + // If the nested bucket doesn't exist, there's no need to delete. + if fwdPkgBkt.NestedReadWriteBucket(sourceBytes[:]) == nil { + return nil + } + + return fwdPkgBkt.DeleteNestedBucket(sourceBytes[:]) +} + +// uint16Key writes the provided 16-bit unsigned integer to a 2-byte slice. +func uint16Key(i uint16) []byte { + key := make([]byte, 2) + byteOrder.PutUint16(key, i) + return key +} + +// Compile-time constraint to ensure that ChannelPackager implements the public +// FwdPackager interface. +var _ FwdPackager = (*ChannelPackager)(nil) + +// Compile-time constraint to ensure that SwitchPackager implements the public +// FwdOperator interface. +var _ FwdOperator = (*SwitchPackager)(nil) diff --git a/chanstate/kv_serialization.go b/chanstate/kv_serialization.go new file mode 100644 index 0000000000..f40e13daaa --- /dev/null +++ b/chanstate/kv_serialization.go @@ -0,0 +1,62 @@ +package chanstate + +import ( + "bytes" + "encoding/binary" + "io" + + "github.com/lightningnetwork/lnd/lnwire" +) + +var byteOrder = binary.BigEndian + +// serializeLogUpdate writes a log update to the provided io.Writer. +func serializeLogUpdate(w io.Writer, l *LogUpdate) error { + if err := binary.Write(w, byteOrder, l.LogIndex); err != nil { + return err + } + + var msgBuf bytes.Buffer + if _, err := lnwire.WriteMessage(&msgBuf, l.UpdateMsg, 0); err != nil { + return err + } + + msgLen := uint16(msgBuf.Len()) + if err := binary.Write(w, byteOrder, msgLen); err != nil { + return err + } + + _, err := w.Write(msgBuf.Bytes()) + + return err +} + +// deserializeLogUpdate reads a log update from the provided io.Reader. +func deserializeLogUpdate(r io.Reader) (*LogUpdate, error) { + l := &LogUpdate{} + if err := binary.Read(r, byteOrder, &l.LogIndex); err != nil { + return nil, err + } + + var msgLen uint16 + if err := binary.Read(r, byteOrder, &msgLen); err != nil { + return nil, err + } + + msgReader := io.LimitReader(r, int64(msgLen)) + msg, err := lnwire.ReadMessage(msgReader, 0) + if err != nil { + return nil, err + } + + l.UpdateMsg = msg + + return l, nil +} + +// makeLogKey converts a uint64 into an 8 byte array. +func makeLogKey(updateNum uint64) [8]byte { + var key [8]byte + byteOrder.PutUint64(key[:], updateNum) + return key +} From 9fdaab04f0af276a2c3866619d12ed6b8f535ac5 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:00:14 -0300 Subject: [PATCH 03/61] chanstate: move forwarding kv tests Move the forwarding-package KV tests next to the implementation in chanstate. The change keeps the package references updated while leaving the compatibility aliases in channeldb for existing callers. --- .../kv_forwarding_package_test.go | 111 ++++++++++-------- 1 file changed, 59 insertions(+), 52 deletions(-) rename channeldb/forwarding_package_test.go => chanstate/kv_forwarding_package_test.go (90%) diff --git a/channeldb/forwarding_package_test.go b/chanstate/kv_forwarding_package_test.go similarity index 90% rename from channeldb/forwarding_package_test.go rename to chanstate/kv_forwarding_package_test.go index 5f47336f69..32928dc9e9 100644 --- a/channeldb/forwarding_package_test.go +++ b/chanstate/kv_forwarding_package_test.go @@ -1,4 +1,4 @@ -package channeldb_test +package chanstate_test import ( "bytes" @@ -7,7 +7,7 @@ import ( "testing" "github.com/btcsuite/btcd/wire/v2" - "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" @@ -27,7 +27,7 @@ func TestPkgFilterBruteForce(t *testing.T) { // properly for all relevant sizes of `high`. func checkPkgFilterRange(t *testing.T, high int) { for i := uint16(0); i < uint16(high); i++ { - f := channeldb.NewPkgFilter(i) + f := chanstate.NewPkgFilter(i) if f.Count() != i { t.Fatalf("pkg filter count=%d is actually %d", @@ -74,7 +74,7 @@ func TestPkgFilterRand(t *testing.T) { // is parameterized by a base `b` coprime to `p`, and using modular // exponentiation to generate all elements in [1,p). func checkPkgFilterRand(t *testing.T, b, p uint16) { - f := channeldb.NewPkgFilter(p) + f := chanstate.NewPkgFilter(p) var j = b for i := uint16(1); i < p; i++ { if f.Contains(j) { @@ -113,7 +113,9 @@ func checkPkgFilterRand(t *testing.T, b, p uint16) { // 2. verifying the number of bytes written matches the filter's Size() // 3. reconstructing the filter decoding the bytes // 4. checking that the two filters are the same according to Equal -func checkPkgFilterEncodeDecode(t *testing.T, i uint16, f *channeldb.PkgFilter) { +func checkPkgFilterEncodeDecode(t *testing.T, i uint16, + f *chanstate.PkgFilter) { + var b bytes.Buffer if err := f.Encode(&b); err != nil { t.Fatalf("unable to serialize pkg filter: %v", err) @@ -128,7 +130,7 @@ func checkPkgFilterEncodeDecode(t *testing.T, i uint16, f *channeldb.PkgFilter) reader := bytes.NewReader(b.Bytes()) - f2 := &channeldb.PkgFilter{} + f2 := &chanstate.PkgFilter{} if err := f2.Decode(reader); err != nil { t.Fatalf("unable to deserialize pkg filter: %v", err) } @@ -144,8 +146,8 @@ var ( chanID = lnwire.NewChanIDFromOutPoint(wire.OutPoint{}) ) -func testSettleFails() []channeldb.LogUpdate { - return []channeldb.LogUpdate{ +func testSettleFails() []chanstate.LogUpdate { + return []chanstate.LogUpdate{ { LogIndex: 2, UpdateMsg: &lnwire.UpdateFulfillHTLC{ @@ -165,8 +167,8 @@ func testSettleFails() []channeldb.LogUpdate { } } -func testAdds() []channeldb.LogUpdate { - return []channeldb.LogUpdate{ +func testAdds() []chanstate.LogUpdate { + return []chanstate.LogUpdate{ { LogIndex: 0, UpdateMsg: &lnwire.UpdateAddHTLC{ @@ -200,7 +202,7 @@ func TestPackagerEmptyFwdPkg(t *testing.T) { db := makeFwdPkgDB(t, "") shortChanID := lnwire.NewShortChanIDFromInt(1) - packager := channeldb.NewChannelPackager(shortChanID) + packager := chanstate.NewChannelPackager(shortChanID) // To begin, there should be no forwarding packages on disk. fwdPkgs := loadFwdPkgs(t, db, packager) @@ -209,7 +211,7 @@ func TestPackagerEmptyFwdPkg(t *testing.T) { } // Next, create and write a new forwarding package with no htlcs. - fwdPkg := channeldb.NewFwdPkg(shortChanID, 0, nil, nil) + fwdPkg := chanstate.NewFwdPkg(shortChanID, 0, nil, nil) if err := kvdb.Update(db, func(tx kvdb.RwTx) error { return packager.AddFwdPkg(tx, fwdPkg) @@ -224,7 +226,7 @@ func TestPackagerEmptyFwdPkg(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateLockedIn) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateLockedIn) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], 0, 0) assertAckFilterIsFull(t, fwdPkgs[0], true) @@ -243,7 +245,7 @@ func TestPackagerEmptyFwdPkg(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateCompleted) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateCompleted) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], 0, 0) assertAckFilterIsFull(t, fwdPkgs[0], true) @@ -269,7 +271,7 @@ func TestPackagerOnlyAdds(t *testing.T) { db := makeFwdPkgDB(t, "") shortChanID := lnwire.NewShortChanIDFromInt(1) - packager := channeldb.NewChannelPackager(shortChanID) + packager := chanstate.NewChannelPackager(shortChanID) // To begin, there should be no forwarding packages on disk. fwdPkgs := loadFwdPkgs(t, db, packager) @@ -281,7 +283,7 @@ func TestPackagerOnlyAdds(t *testing.T) { // Next, create and write a new forwarding package that only has add // htlcs. - fwdPkg := channeldb.NewFwdPkg(shortChanID, 0, adds, nil) + fwdPkg := chanstate.NewFwdPkg(shortChanID, 0, adds, nil) nAdds := len(adds) @@ -298,7 +300,7 @@ func TestPackagerOnlyAdds(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateLockedIn) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateLockedIn) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, 0) assertAckFilterIsFull(t, fwdPkgs[0], false) @@ -321,11 +323,11 @@ func TestPackagerOnlyAdds(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateProcessed) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateProcessed) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, 0) assertAckFilterIsFull(t, fwdPkgs[0], false) - addRef := channeldb.AddRef{ + addRef := chanstate.AddRef{ Height: fwdPkg.Height, Index: uint16(i), } @@ -344,7 +346,7 @@ func TestPackagerOnlyAdds(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateCompleted) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateCompleted) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, 0) assertAckFilterIsFull(t, fwdPkgs[0], true) @@ -373,7 +375,7 @@ func TestPackagerOnlySettleFails(t *testing.T) { db := makeFwdPkgDB(t, "") shortChanID := lnwire.NewShortChanIDFromInt(1) - packager := channeldb.NewChannelPackager(shortChanID) + packager := chanstate.NewChannelPackager(shortChanID) // To begin, there should be no forwarding packages on disk. fwdPkgs := loadFwdPkgs(t, db, packager) @@ -384,7 +386,7 @@ func TestPackagerOnlySettleFails(t *testing.T) { // Next, create and write a new forwarding package that only has add // htlcs. settleFails := testSettleFails() - fwdPkg := channeldb.NewFwdPkg(shortChanID, 0, nil, settleFails) + fwdPkg := chanstate.NewFwdPkg(shortChanID, 0, nil, settleFails) nSettleFails := len(settleFails) @@ -401,7 +403,7 @@ func TestPackagerOnlySettleFails(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateLockedIn) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateLockedIn) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], 0, nSettleFails) assertAckFilterIsFull(t, fwdPkgs[0], true) @@ -424,12 +426,12 @@ func TestPackagerOnlySettleFails(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateProcessed) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateProcessed) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], 0, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], false) assertAckFilterIsFull(t, fwdPkgs[0], true) - failSettleRef := channeldb.SettleFailRef{ + failSettleRef := chanstate.SettleFailRef{ Source: shortChanID, Height: fwdPkg.Height, Index: uint16(i), @@ -449,7 +451,7 @@ func TestPackagerOnlySettleFails(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateCompleted) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateCompleted) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], 0, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], true) assertAckFilterIsFull(t, fwdPkgs[0], true) @@ -478,7 +480,7 @@ func TestPackagerAddsThenSettleFails(t *testing.T) { db := makeFwdPkgDB(t, "") shortChanID := lnwire.NewShortChanIDFromInt(1) - packager := channeldb.NewChannelPackager(shortChanID) + packager := chanstate.NewChannelPackager(shortChanID) // To begin, there should be no forwarding packages on disk. fwdPkgs := loadFwdPkgs(t, db, packager) @@ -491,7 +493,7 @@ func TestPackagerAddsThenSettleFails(t *testing.T) { // Next, create and write a new forwarding package that only has add // htlcs. settleFails := testSettleFails() - fwdPkg := channeldb.NewFwdPkg(shortChanID, 0, adds, settleFails) + fwdPkg := chanstate.NewFwdPkg(shortChanID, 0, adds, settleFails) nAdds := len(adds) nSettleFails := len(settleFails) @@ -509,7 +511,7 @@ func TestPackagerAddsThenSettleFails(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateLockedIn) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateLockedIn) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertAckFilterIsFull(t, fwdPkgs[0], false) @@ -532,12 +534,12 @@ func TestPackagerAddsThenSettleFails(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateProcessed) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateProcessed) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], false) assertAckFilterIsFull(t, fwdPkgs[0], false) - addRef := channeldb.AddRef{ + addRef := chanstate.AddRef{ Height: fwdPkg.Height, Index: uint16(i), } @@ -558,12 +560,12 @@ func TestPackagerAddsThenSettleFails(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateProcessed) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateProcessed) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], false) assertAckFilterIsFull(t, fwdPkgs[0], true) - failSettleRef := channeldb.SettleFailRef{ + failSettleRef := chanstate.SettleFailRef{ Source: shortChanID, Height: fwdPkg.Height, Index: uint16(i), @@ -583,7 +585,7 @@ func TestPackagerAddsThenSettleFails(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateCompleted) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateCompleted) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], true) assertAckFilterIsFull(t, fwdPkgs[0], true) @@ -614,7 +616,7 @@ func TestPackagerSettleFailsThenAdds(t *testing.T) { db := makeFwdPkgDB(t, "") shortChanID := lnwire.NewShortChanIDFromInt(1) - packager := channeldb.NewChannelPackager(shortChanID) + packager := chanstate.NewChannelPackager(shortChanID) // To begin, there should be no forwarding packages on disk. fwdPkgs := loadFwdPkgs(t, db, packager) @@ -627,7 +629,7 @@ func TestPackagerSettleFailsThenAdds(t *testing.T) { // Next, create and write a new forwarding package that has both add // and settle/fail htlcs. settleFails := testSettleFails() - fwdPkg := channeldb.NewFwdPkg(shortChanID, 0, adds, settleFails) + fwdPkg := chanstate.NewFwdPkg(shortChanID, 0, adds, settleFails) nAdds := len(adds) nSettleFails := len(settleFails) @@ -645,7 +647,7 @@ func TestPackagerSettleFailsThenAdds(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateLockedIn) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateLockedIn) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertAckFilterIsFull(t, fwdPkgs[0], false) @@ -671,12 +673,12 @@ func TestPackagerSettleFailsThenAdds(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateProcessed) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateProcessed) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], false) assertAckFilterIsFull(t, fwdPkgs[0], false) - failSettleRef := channeldb.SettleFailRef{ + failSettleRef := chanstate.SettleFailRef{ Source: shortChanID, Height: fwdPkg.Height, Index: uint16(i), @@ -699,12 +701,12 @@ func TestPackagerSettleFailsThenAdds(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateProcessed) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateProcessed) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], true) assertAckFilterIsFull(t, fwdPkgs[0], false) - addRef := channeldb.AddRef{ + addRef := chanstate.AddRef{ Height: fwdPkg.Height, Index: uint16(i), } @@ -723,7 +725,7 @@ func TestPackagerSettleFailsThenAdds(t *testing.T) { if len(fwdPkgs) != 1 { t.Fatalf("expected 1 fwdpkg, instead found %d", len(fwdPkgs)) } - assertFwdPkgState(t, fwdPkgs[0], channeldb.FwdStateCompleted) + assertFwdPkgState(t, fwdPkgs[0], chanstate.FwdStateCompleted) assertFwdPkgNumAddsSettleFails(t, fwdPkgs[0], nAdds, nSettleFails) assertSettleFailFilterIsFull(t, fwdPkgs[0], true) assertAckFilterIsFull(t, fwdPkgs[0], true) @@ -750,7 +752,7 @@ func TestPackagerWipeAll(t *testing.T) { db := makeFwdPkgDB(t, "") shortChanID := lnwire.NewShortChanIDFromInt(1) - packager := channeldb.NewChannelPackager(shortChanID) + packager := chanstate.NewChannelPackager(shortChanID) // To begin, there should be no forwarding packages on disk. fwdPkgs := loadFwdPkgs(t, db, packager) @@ -761,8 +763,8 @@ func TestPackagerWipeAll(t *testing.T) { require.NoError(t, err, "unable to wipe fwdpkg") // Next, create and write two forwarding packages with no htlcs. - fwdPkg1 := channeldb.NewFwdPkg(shortChanID, 0, nil, nil) - fwdPkg2 := channeldb.NewFwdPkg(shortChanID, 1, nil, nil) + fwdPkg1 := chanstate.NewFwdPkg(shortChanID, 0, nil, nil) + fwdPkg2 := chanstate.NewFwdPkg(shortChanID, 1, nil, nil) err = kvdb.Update(db, func(tx kvdb.RwTx) error { if err := packager.AddFwdPkg(tx, fwdPkg2); err != nil { @@ -787,8 +789,9 @@ func TestPackagerWipeAll(t *testing.T) { // assertFwdPkgState checks the current state of a fwdpkg meets our // expectations. -func assertFwdPkgState(t *testing.T, fwdPkg *channeldb.FwdPkg, - state channeldb.FwdState) { +func assertFwdPkgState(t *testing.T, fwdPkg *chanstate.FwdPkg, + state chanstate.FwdState) { + _, _, line, _ := runtime.Caller(1) if fwdPkg.State != state { t.Fatalf("line %d: expected fwdpkg in state %v, found %v", @@ -798,7 +801,7 @@ func assertFwdPkgState(t *testing.T, fwdPkg *channeldb.FwdPkg, // assertFwdPkgNumAddsSettleFails checks that the number of adds and // settle/fail log updates are correct. -func assertFwdPkgNumAddsSettleFails(t *testing.T, fwdPkg *channeldb.FwdPkg, +func assertFwdPkgNumAddsSettleFails(t *testing.T, fwdPkg *chanstate.FwdPkg, expectedNumAdds, expectedNumSettleFails int) { _, _, line, _ := runtime.Caller(1) if len(fwdPkg.Adds) != expectedNumAdds { @@ -814,7 +817,9 @@ func assertFwdPkgNumAddsSettleFails(t *testing.T, fwdPkg *channeldb.FwdPkg, // assertAckFilterIsFull checks whether or not a fwdpkg's ack filter matches our // expected full-ness. -func assertAckFilterIsFull(t *testing.T, fwdPkg *channeldb.FwdPkg, expected bool) { +func assertAckFilterIsFull(t *testing.T, fwdPkg *chanstate.FwdPkg, + expected bool) { + _, _, line, _ := runtime.Caller(1) if fwdPkg.AckFilter.IsFull() != expected { t.Fatalf("line %d: expected fwdpkg ack filter IsFull to be %v, "+ @@ -824,7 +829,9 @@ func assertAckFilterIsFull(t *testing.T, fwdPkg *channeldb.FwdPkg, expected bool // assertSettleFailFilterIsFull checks whether or not a fwdpkg's settle fail // filter matches our expected full-ness. -func assertSettleFailFilterIsFull(t *testing.T, fwdPkg *channeldb.FwdPkg, expected bool) { +func assertSettleFailFilterIsFull(t *testing.T, fwdPkg *chanstate.FwdPkg, + expected bool) { + _, _, line, _ := runtime.Caller(1) if fwdPkg.SettleFailFilter.IsFull() != expected { t.Fatalf("line %d: expected fwdpkg settle/fail filter IsFull to be %v, "+ @@ -835,9 +842,9 @@ func assertSettleFailFilterIsFull(t *testing.T, fwdPkg *channeldb.FwdPkg, expect // loadFwdPkgs is a helper method that reads all forwarding packages for a // particular packager. func loadFwdPkgs(t *testing.T, db kvdb.Backend, - packager channeldb.FwdPackager) []*channeldb.FwdPkg { + packager chanstate.FwdPackager) []*chanstate.FwdPkg { - var fwdPkgs []*channeldb.FwdPkg + var fwdPkgs []*chanstate.FwdPkg if err := kvdb.View(db, func(tx kvdb.RTx) error { var err error fwdPkgs, err = packager.LoadFwdPkgs(tx) From 7141e13561b5f9000e197e9e6aee7855c55eebd9 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:07:23 -0300 Subject: [PATCH 04/61] chanstate: move revocation log kv helpers Move the current revocation-log KV put and fetch helpers into chanstate while leaving deprecated-bucket compatibility in channeldb. The deprecated path still depends on the old commitment decoder, so it remains with the channeldb commitment serialization until that store is moved separately. --- channeldb/revocation_log.go | 85 ++----------------------- chanstate/kv_revocation_log.go | 113 +++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 78 deletions(-) diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 8340c8b19b..988ae1f25d 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -2,9 +2,7 @@ package channeldb import ( "bytes" - "errors" "io" - "math" cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/kvdb" @@ -64,15 +62,15 @@ var ( // sub-bucket is dedicated for storing the minimal info required to // re-construct a past state in order to punish a counterparty // attempting a non-cooperative channel closure. - revocationLogBucket = []byte("revocation-log") + revocationLogBucket = cstate.RevocationLogBucketKey() // ErrLogEntryNotFound is returned when we cannot find a log entry at // the height requested in the revocation log. - ErrLogEntryNotFound = errors.New("log entry not found") + ErrLogEntryNotFound = cstate.ErrLogEntryNotFound // ErrOutputIndexTooBig is returned when the output index is greater // than uint16. - ErrOutputIndexTooBig = errors.New("output index is over uint16") + ErrOutputIndexTooBig = cstate.ErrOutputIndexTooBig ) // putRevocationLog uses the fields `CommitTx` and `Htlcs` from a @@ -82,70 +80,9 @@ var ( func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, ourOutputIndex, theirOutputIndex uint32, noAmtData bool) error { - // Sanity check that the output indexes can be safely converted. - if ourOutputIndex > math.MaxUint16 { - return ErrOutputIndexTooBig - } - if theirOutputIndex > math.MaxUint16 { - return ErrOutputIndexTooBig - } - - rl := &RevocationLog{ - OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0]( - uint16(ourOutputIndex), - ), - TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1]( - uint16(theirOutputIndex), - ), - CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2, [32]byte]( - commit.CommitTx.TxHash(), - ), - HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), - } - - commit.CustomBlob.WhenSome(func(blob tlv.Blob) { - rl.CustomBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), - ) - }) - - if !noAmtData { - rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3]( - tlv.NewBigSizeT(commit.LocalBalance), - )) - - rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4]( - tlv.NewBigSizeT(commit.RemoteBalance), - )) - } - - for _, htlc := range commit.Htlcs { - // Skip dust HTLCs. - if htlc.OutputIndex < 0 { - continue - } - - // Sanity check that the output indexes can be safely - // converted. - if htlc.OutputIndex > math.MaxUint16 { - return ErrOutputIndexTooBig - } - - entry, err := NewHTLCEntryFromHTLC(htlc) - if err != nil { - return err - } - rl.HTLCEntries = append(rl.HTLCEntries, entry) - } - - var b bytes.Buffer - err := serializeRevocationLog(&b, rl) - if err != nil { - return err - } - - logEntrykey := makeLogKey(commit.CommitHeight) - return bucket.Put(logEntrykey[:], b.Bytes()) + return cstate.PutRevocationLog( + bucket, commit, ourOutputIndex, theirOutputIndex, noAmtData, + ) } // fetchRevocationLog queries the revocation log bucket to find an log entry. @@ -153,15 +90,7 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, func fetchRevocationLog(log kvdb.RBucket, updateNum uint64) (RevocationLog, error) { - logEntrykey := makeLogKey(updateNum) - commitBytes := log.Get(logEntrykey[:]) - if commitBytes == nil { - return RevocationLog{}, ErrLogEntryNotFound - } - - commitReader := bytes.NewReader(commitBytes) - - return deserializeRevocationLog(commitReader) + return cstate.FetchRevocationLog(log, updateNum) } // serializeRevocationLog serializes a RevocationLog record based on tlv diff --git a/chanstate/kv_revocation_log.go b/chanstate/kv_revocation_log.go index 37bae338b3..30124f51db 100644 --- a/chanstate/kv_revocation_log.go +++ b/chanstate/kv_revocation_log.go @@ -5,13 +5,126 @@ import ( "encoding/binary" "errors" "io" + "math" + "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/tlv" ) // This file contains the KV/TLV serialization helpers for revocation logs. // The domain types remain in revocation_log.go. +var ( + // revocationLogBucket is a sub-bucket under openChannelBucket. This + // sub-bucket is dedicated for storing the minimal info required to + // re-construct a past state in order to punish a counterparty + // attempting a non-cooperative channel closure. + revocationLogBucket = []byte("revocation-log") + + // ErrLogEntryNotFound is returned when we cannot find a log entry at + // the height requested in the revocation log. + ErrLogEntryNotFound = errors.New("log entry not found") + + // ErrOutputIndexTooBig is returned when the output index is greater + // than uint16. + ErrOutputIndexTooBig = errors.New("output index is over uint16") +) + +// RevocationLogBucketKey returns the sub-bucket key that stores the current +// revocation log format. +func RevocationLogBucketKey() []byte { + return revocationLogBucket +} + +// PutRevocationLog uses the fields `CommitTx` and `Htlcs` from a +// ChannelCommitment to construct a revocation log entry and saves them to disk. +// It also saves our output index and their output index, which are useful when +// creating breach retribution. +func PutRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, + ourOutputIndex, theirOutputIndex uint32, noAmtData bool) error { + + // Sanity check that the output indexes can be safely converted. + if ourOutputIndex > math.MaxUint16 { + return ErrOutputIndexTooBig + } + if theirOutputIndex > math.MaxUint16 { + return ErrOutputIndexTooBig + } + + rl := &RevocationLog{ + OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0]( + uint16(ourOutputIndex), + ), + TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1]( + uint16(theirOutputIndex), + ), + CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2, [32]byte]( + commit.CommitTx.TxHash(), + ), + HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), + } + + commit.CustomBlob.WhenSome(func(blob tlv.Blob) { + rl.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + }) + + if !noAmtData { + rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3]( + tlv.NewBigSizeT(commit.LocalBalance), + )) + + rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4]( + tlv.NewBigSizeT(commit.RemoteBalance), + )) + } + + for _, htlc := range commit.Htlcs { + // Skip dust HTLCs. + if htlc.OutputIndex < 0 { + continue + } + + // Sanity check that the output indexes can be safely converted. + if htlc.OutputIndex > math.MaxUint16 { + return ErrOutputIndexTooBig + } + + entry, err := NewHTLCEntryFromHTLC(htlc) + if err != nil { + return err + } + rl.HTLCEntries = append(rl.HTLCEntries, entry) + } + + var b bytes.Buffer + err := SerializeRevocationLog(&b, rl) + if err != nil { + return err + } + + logEntrykey := makeLogKey(commit.CommitHeight) + + return bucket.Put(logEntrykey[:], b.Bytes()) +} + +// FetchRevocationLog queries the revocation log bucket to find an log entry. +// Return an error if not found. +func FetchRevocationLog(log kvdb.RBucket, + updateNum uint64) (RevocationLog, error) { + + logEntrykey := makeLogKey(updateNum) + commitBytes := log.Get(logEntrykey[:]) + if commitBytes == nil { + return RevocationLog{}, ErrLogEntryNotFound + } + + commitReader := bytes.NewReader(commitBytes) + + return DeserializeRevocationLog(commitReader) +} + // htlcEntryToTlvStream converts an HTLCEntry record into a tlv representation. func htlcEntryToTlvStream(h *HTLCEntry) (*tlv.Stream, error) { records := []tlv.Record{ From 46d706450caedeeb62bdbc20e55fc4c307d2ce30 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:14:09 -0300 Subject: [PATCH 05/61] chanstate: move final htlc kv helpers Move the final-HTLC bucket helpers, byte encoding, and log-update processing into chanstate so the KV-specific implementation lives with the channel state package. Keep channeldb wrappers and aliases in place so existing callers and tests continue to compile while the remaining channel-state code moves over in later commits. --- channeldb/channel.go | 102 ++----------------- channeldb/db.go | 52 +++------- chanstate/kv_final_htlc.go | 195 +++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 134 deletions(-) create mode 100644 chanstate/kv_final_htlc.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 91daa5430c..157efd5fcf 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -11,7 +11,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/wire/v2" - "github.com/btcsuite/btcwallet/walletdb" cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" @@ -146,26 +145,6 @@ var ( // sent was a revocation and false when it was a commitment signature. // This is nil in the case of new channels with no updates exchanged. lastWasRevokeKey = []byte("last-was-revoke") - - // finalHtlcsBucket contains the htlcs that have been resolved - // definitively. Within this bucket, there is a sub-bucket for each - // channel. In each channel bucket, the htlc indices are stored along - // with final outcome. - // - // final-htlcs -> chanID -> htlcIndex -> outcome - // - // 'outcome' is a byte value that encodes: - // - // | true false - // ------+------------------ - // bit 0 | settled failed - // bit 1 | offchain onchain - // - // This bucket is positioned at the root level, because its contents - // will be kept independent of the channel lifecycle. This is to avoid - // the situation where a channel force-closes autonomously and the user - // not being able to query for htlc outcomes anymore. - finalHtlcsBucket = []byte("final-htlcs") ) var ( @@ -591,18 +570,18 @@ var ( ChanStatusRemoteCloseInitiator = cstate.ChanStatusRemoteCloseInitiator ) -// FinalHtlcByte defines a byte type that encodes information about the final -// htlc resolution. -type FinalHtlcByte byte +// FinalHtlcByte is a type alias for a byte that encodes information about the +// final htlc resolution. +type FinalHtlcByte = cstate.FinalHtlcByte const ( // FinalHtlcSettledBit is the bit that encodes whether the htlc was // settled or failed. - FinalHtlcSettledBit FinalHtlcByte = 1 << 0 + FinalHtlcSettledBit = cstate.FinalHtlcSettledBit // FinalHtlcOffchainBit is the bit that encodes whether the htlc was // resolved offchain or onchain. - FinalHtlcOffchainBit FinalHtlcByte = 1 << 1 + FinalHtlcOffchainBit = cstate.FinalHtlcOffchainBit ) // amendOpenChannelTlvData updates the channel with the given auxiliary TLV @@ -835,21 +814,7 @@ func fetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey, func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, chanID lnwire.ShortChannelID) (kvdb.RwBucket, error) { - finalHtlcsBucket, err := tx.CreateTopLevelBucket(finalHtlcsBucket) - if err != nil { - return nil, err - } - - var chanIDBytes [8]byte - byteOrder.PutUint64(chanIDBytes[:], chanID.ToUint64()) - chanBucket, err := finalHtlcsBucket.CreateBucketIfNotExists( - chanIDBytes[:], - ) - if err != nil { - return nil, err - } - - return chanBucket, nil + return cstate.FetchFinalHtlcsBucketRw(tx, chanID) } // fullSyncOpenChannel syncs the contents of an OpenChannel while re-using an @@ -1702,48 +1667,10 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, // processFinalHtlc stores a final htlc outcome in the database if signaled via // the supplied log update. An in-memory htlcs map is updated too. -func processFinalHtlc(finalHtlcsBucket walletdb.ReadWriteBucket, upd LogUpdate, +func processFinalHtlc(finalHtlcsBucket kvdb.RwBucket, upd LogUpdate, finalHtlcs map[uint64]bool) error { - var ( - settled bool - id uint64 - ) - - switch msg := upd.UpdateMsg.(type) { - case *lnwire.UpdateFulfillHTLC: - settled = true - id = msg.ID - - case *lnwire.UpdateFailHTLC: - settled = false - id = msg.ID - - case *lnwire.UpdateFailMalformedHTLC: - settled = false - id = msg.ID - - default: - return nil - } - - // Store the final resolution in the database if a bucket is provided. - if finalHtlcsBucket != nil { - err := putFinalHtlc( - finalHtlcsBucket, id, - FinalHtlcInfo{ - Settled: settled, - Offchain: true, - }, - ) - if err != nil { - return err - } - } - - finalHtlcs[id] = settled - - return nil + return cstate.ProcessFinalHtlc(finalHtlcsBucket, upd, finalHtlcs) } // serializeHtlcExtraData encodes a TLV stream of extra data to be stored with a @@ -2479,18 +2406,7 @@ type FinalHtlcInfo = cstate.FinalHtlcInfo func putFinalHtlc(finalHtlcsBucket kvdb.RwBucket, id uint64, info FinalHtlcInfo) error { - var key [8]byte - byteOrder.PutUint64(key[:], id) - - var finalHtlcByte FinalHtlcByte - if info.Settled { - finalHtlcByte |= FinalHtlcSettledBit - } - if info.Offchain { - finalHtlcByte |= FinalHtlcOffchainBit - } - - return finalHtlcsBucket.Put(key[:], []byte{byte(finalHtlcByte)}) + return cstate.PutFinalHtlc(finalHtlcsBucket, id, info) } // LoadFwdPkgs scans the forwarding log for any packages that haven't been diff --git a/channeldb/db.go b/channeldb/db.go index a66e8f19fb..86fae93b3a 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -52,13 +52,11 @@ var ( // ErrFinalHtlcsBucketNotFound signals that the top-level final htlcs // bucket does not exist. - ErrFinalHtlcsBucketNotFound = errors.New("final htlcs bucket not " + - "found") + ErrFinalHtlcsBucketNotFound = chanstate.ErrFinalHtlcsBucketNotFound // ErrFinalChannelBucketNotFound signals that the channel bucket for // final htlc outcomes does not exist. - ErrFinalChannelBucketNotFound = errors.New("final htlcs channel " + - "bucket not found") + ErrFinalChannelBucketNotFound = chanstate.ErrFinalChannelBucketNotFound ) // migration is a function which takes a prior outdated version of the database @@ -2107,33 +2105,17 @@ func (c *ChannelStateDB) FetchHistoricalChannel(outPoint *wire.OutPoint) ( func fetchFinalHtlcsBucket(tx kvdb.RTx, chanID lnwire.ShortChannelID) (kvdb.RBucket, error) { - finalHtlcsBucket := tx.ReadBucket(finalHtlcsBucket) - if finalHtlcsBucket == nil { - return nil, ErrFinalHtlcsBucketNotFound - } - - var chanIDBytes [8]byte - byteOrder.PutUint64(chanIDBytes[:], chanID.ToUint64()) - - chanBucket := finalHtlcsBucket.NestedReadBucket(chanIDBytes[:]) - if chanBucket == nil { - return nil, ErrFinalChannelBucketNotFound - } - - return chanBucket, nil + return chanstate.FetchFinalHtlcsBucket(tx, chanID) } -var ErrHtlcUnknown = errors.New("htlc unknown") +var ErrHtlcUnknown = chanstate.ErrHtlcUnknown // LookupFinalHtlc retrieves a final htlc resolution from the database. If the // htlc has no final resolution yet, ErrHtlcUnknown is returned. func (c *ChannelStateDB) LookupFinalHtlc(chanID lnwire.ShortChannelID, htlcIndex uint64) (*FinalHtlcInfo, error) { - var idBytes [8]byte - byteOrder.PutUint64(idBytes[:], htlcIndex) - - var settledByte byte + var info *FinalHtlcInfo err := kvdb.View(c.backend, func(tx kvdb.RTx) error { finalHtlcsBucket, err := fetchFinalHtlcsBucket( @@ -2151,31 +2133,19 @@ func (c *ChannelStateDB) LookupFinalHtlc(chanID lnwire.ShortChannelID, err) } - value := finalHtlcsBucket.Get(idBytes[:]) - if value == nil { - return ErrHtlcUnknown - } - - if len(value) != 1 { - return errors.New("unexpected final htlc value length") - } - - settledByte = value[0] + info, err = chanstate.FetchFinalHtlc( + finalHtlcsBucket, htlcIndex, + ) - return nil + return err }, func() { - settledByte = 0 + info = nil }) if err != nil { return nil, err } - info := FinalHtlcInfo{ - Settled: settledByte&byte(FinalHtlcSettledBit) != 0, - Offchain: settledByte&byte(FinalHtlcOffchainBit) != 0, - } - - return &info, nil + return info, nil } // PutOnchainFinalHtlcOutcome stores the final on-chain outcome of an htlc in diff --git a/chanstate/kv_final_htlc.go b/chanstate/kv_final_htlc.go new file mode 100644 index 0000000000..e3de8007d5 --- /dev/null +++ b/chanstate/kv_final_htlc.go @@ -0,0 +1,195 @@ +package chanstate + +import ( + "errors" + + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" +) + +var ( + // ErrFinalHtlcsBucketNotFound signals that the top-level final htlcs + // bucket does not exist. + ErrFinalHtlcsBucketNotFound = errors.New("final htlcs bucket not " + + "found") + + // ErrFinalChannelBucketNotFound signals that the channel bucket for + // final htlc outcomes does not exist. + ErrFinalChannelBucketNotFound = errors.New("final htlcs channel " + + "bucket not found") + + // ErrHtlcUnknown signals that an htlc has no final resolution yet. + ErrHtlcUnknown = errors.New("htlc unknown") +) + +var ( + // finalHtlcsBucket contains the htlcs that have been resolved + // definitively. Within this bucket, there is a sub-bucket for each + // channel. In each channel bucket, the htlc indices are stored along + // with final outcome. + // + // final-htlcs -> chanID -> htlcIndex -> outcome + // + // 'outcome' is a byte value that encodes: + // + // | true false + // ------+------------------ + // bit 0 | settled failed + // bit 1 | offchain onchain + // + // This bucket is positioned at the root level, because its contents + // will be kept independent of the channel lifecycle. This is to avoid + // the situation where a channel force-closes autonomously and the user + // not being able to query for htlc outcomes anymore. + finalHtlcsBucket = []byte("final-htlcs") +) + +// FinalHtlcsBucketKey returns the top-level bucket key that stores final htlc +// outcomes. +func FinalHtlcsBucketKey() []byte { + return finalHtlcsBucket +} + +// FinalHtlcByte defines a byte type that encodes information about the final +// htlc resolution. +type FinalHtlcByte byte + +const ( + // FinalHtlcSettledBit is the bit that encodes whether the htlc was + // settled or failed. + FinalHtlcSettledBit FinalHtlcByte = 1 << 0 + + // FinalHtlcOffchainBit is the bit that encodes whether the htlc was + // resolved offchain or onchain. + FinalHtlcOffchainBit FinalHtlcByte = 1 << 1 +) + +// FetchFinalHtlcsBucket returns the read-only final htlc bucket for a channel. +func FetchFinalHtlcsBucket(tx kvdb.RTx, + chanID lnwire.ShortChannelID) (kvdb.RBucket, error) { + + finalHtlcsBucket := tx.ReadBucket(finalHtlcsBucket) + if finalHtlcsBucket == nil { + return nil, ErrFinalHtlcsBucketNotFound + } + + var chanIDBytes [8]byte + byteOrder.PutUint64(chanIDBytes[:], chanID.ToUint64()) + + chanBucket := finalHtlcsBucket.NestedReadBucket(chanIDBytes[:]) + if chanBucket == nil { + return nil, ErrFinalChannelBucketNotFound + } + + return chanBucket, nil +} + +// FetchFinalHtlcsBucketRw returns the writable final htlc bucket for a channel. +func FetchFinalHtlcsBucketRw(tx kvdb.RwTx, + chanID lnwire.ShortChannelID) (kvdb.RwBucket, error) { + + finalHtlcsBucket, err := tx.CreateTopLevelBucket(finalHtlcsBucket) + if err != nil { + return nil, err + } + + var chanIDBytes [8]byte + byteOrder.PutUint64(chanIDBytes[:], chanID.ToUint64()) + chanBucket, err := finalHtlcsBucket.CreateBucketIfNotExists( + chanIDBytes[:], + ) + if err != nil { + return nil, err + } + + return chanBucket, nil +} + +// PutFinalHtlc writes the final htlc outcome to the database. Additionally it +// records whether the htlc was resolved off-chain or on-chain. +func PutFinalHtlc(finalHtlcsBucket kvdb.RwBucket, id uint64, + info FinalHtlcInfo) error { + + var key [8]byte + byteOrder.PutUint64(key[:], id) + + var finalHtlcByte FinalHtlcByte + if info.Settled { + finalHtlcByte |= FinalHtlcSettledBit + } + if info.Offchain { + finalHtlcByte |= FinalHtlcOffchainBit + } + + return finalHtlcsBucket.Put(key[:], []byte{byte(finalHtlcByte)}) +} + +// FetchFinalHtlc reads a final htlc outcome from the final htlc channel bucket. +func FetchFinalHtlc(finalHtlcsBucket kvdb.RBucket, + htlcIndex uint64) (*FinalHtlcInfo, error) { + + var idBytes [8]byte + byteOrder.PutUint64(idBytes[:], htlcIndex) + + value := finalHtlcsBucket.Get(idBytes[:]) + if value == nil { + return nil, ErrHtlcUnknown + } + + if len(value) != 1 { + return nil, errors.New("unexpected final htlc value length") + } + + info := FinalHtlcInfo{ + Settled: value[0]&byte(FinalHtlcSettledBit) != 0, + Offchain: value[0]&byte(FinalHtlcOffchainBit) != 0, + } + + return &info, nil +} + +// ProcessFinalHtlc stores a final htlc outcome in the database if signaled via +// the supplied log update. An in-memory htlcs map is updated too. +func ProcessFinalHtlc(finalHtlcsBucket kvdb.RwBucket, upd LogUpdate, + finalHtlcs map[uint64]bool) error { + + var ( + settled bool + id uint64 + ) + + switch msg := upd.UpdateMsg.(type) { + case *lnwire.UpdateFulfillHTLC: + settled = true + id = msg.ID + + case *lnwire.UpdateFailHTLC: + settled = false + id = msg.ID + + case *lnwire.UpdateFailMalformedHTLC: + settled = false + id = msg.ID + + default: + return nil + } + + // Store the final resolution in the database if a bucket is provided. + if finalHtlcsBucket != nil { + err := PutFinalHtlc( + finalHtlcsBucket, id, + FinalHtlcInfo{ + Settled: settled, + Offchain: true, + }, + ) + if err != nil { + return err + } + } + + finalHtlcs[id] = settled + + return nil +} From 1a47fbd8cb540e6a6c501fc2be6b09fc685d9a23 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:25:18 -0300 Subject: [PATCH 06/61] chanstate: move shutdown kv helpers Move the shutdown-info key, serialization, and channel-bucket put/fetch helpers into chanstate. Leave ChannelStateDB responsible for opening the channel bucket so this commit only moves the backend-specific record handling for shutdown state. --- channeldb/channel.go | 54 +++----------------------- chanstate/kv_shutdown.go | 82 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 chanstate/kv_shutdown.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 157efd5fcf..c997fb0b62 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -121,12 +121,6 @@ var ( // broadcasted when moving the channel to state CoopBroadcasted. coopCloseTxKey = []byte("coop-closing-tx-key") - // shutdownInfoKey points to the serialised shutdown info that has been - // persisted for a channel. The existence of this info means that we - // have sent the Shutdown message before and so should re-initiate the - // shutdown on re-establish. - shutdownInfoKey = []byte("shutdown-info-key") - // commitDiffKey stores the current pending commitment state we've // extended to the remote party (if any). Each time we propose a new // state, we store the information necessary to reconstruct this state @@ -1120,12 +1114,6 @@ var ( func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, info *ShutdownInfo) error { - var b bytes.Buffer - err := encodeShutdownInfo(info, &b) - if err != nil { - return err - } - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, @@ -1135,7 +1123,7 @@ func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, return err } - return chanBucket.Put(shutdownInfoKey, b.Bytes()) + return cstate.PutChannelShutdownInfo(chanBucket, info) }, func() {}) } @@ -1161,12 +1149,7 @@ func (c *ChannelStateDB) FetchChannelShutdownInfo( return err } - shutdownInfoBytes := chanBucket.Get(shutdownInfoKey) - if shutdownInfoBytes == nil { - return ErrNoShutdownInfo - } - - shutdownInfo, err = decodeShutdownInfo(shutdownInfoBytes) + shutdownInfo, err = cstate.FetchChannelShutdownInfo(chanBucket) return err }, func() { @@ -3546,35 +3529,8 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { type ShutdownInfo = cstate.ShutdownInfo // NewShutdownInfo constructs a new ShutdownInfo object. -var NewShutdownInfo = cstate.NewShutdownInfo - -// encodeShutdownInfo serialises the ShutdownInfo to the given io.Writer. -func encodeShutdownInfo(s *ShutdownInfo, w io.Writer) error { - records := []tlv.Record{ - s.DeliveryScript.Record(), - s.LocalInitiator.Record(), - } - - stream, err := tlv.NewStream(records...) - if err != nil { - return err - } - - return stream.Encode(w) -} - -// decodeShutdownInfo constructs a ShutdownInfo struct by decoding the given -// byte slice. -func decodeShutdownInfo(b []byte) (*ShutdownInfo, error) { - tlvStream := lnwire.ExtraOpaqueData(b) - - var info ShutdownInfo - records := []tlv.RecordProducer{ - &info.DeliveryScript, - &info.LocalInitiator, - } - - _, err := tlvStream.ExtractRecords(records...) +func NewShutdownInfo(deliveryScript lnwire.DeliveryAddress, + locallyInitiated bool) *ShutdownInfo { - return &info, err + return cstate.NewShutdownInfo(deliveryScript, locallyInitiated) } diff --git a/chanstate/kv_shutdown.go b/chanstate/kv_shutdown.go new file mode 100644 index 0000000000..55ca0df908 --- /dev/null +++ b/chanstate/kv_shutdown.go @@ -0,0 +1,82 @@ +package chanstate + +import ( + "bytes" + "io" + + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +var ( + // shutdownInfoKey points to the serialised shutdown info that has been + // persisted for a channel. The existence of this info means that we + // have sent the Shutdown message before and so should re-initiate the + // shutdown on re-establish. + shutdownInfoKey = []byte("shutdown-info-key") +) + +// ShutdownInfoKey returns the key for the serialised shutdown info stored in a +// channel bucket. +func ShutdownInfoKey() []byte { + return shutdownInfoKey +} + +// PutChannelShutdownInfo persists the ShutdownInfo in the target channel +// bucket. +func PutChannelShutdownInfo(chanBucket kvdb.RwBucket, + info *ShutdownInfo) error { + + var b bytes.Buffer + err := EncodeShutdownInfo(info, &b) + if err != nil { + return err + } + + return chanBucket.Put(shutdownInfoKey, b.Bytes()) +} + +// FetchChannelShutdownInfo fetches the persisted ShutdownInfo from the target +// channel bucket. +func FetchChannelShutdownInfo(chanBucket kvdb.RBucket) ( + *ShutdownInfo, error) { + + shutdownInfoBytes := chanBucket.Get(shutdownInfoKey) + if shutdownInfoBytes == nil { + return nil, ErrNoShutdownInfo + } + + return DecodeShutdownInfo(shutdownInfoBytes) +} + +// EncodeShutdownInfo serialises the ShutdownInfo to the given io.Writer. +func EncodeShutdownInfo(s *ShutdownInfo, w io.Writer) error { + records := []tlv.Record{ + s.DeliveryScript.Record(), + s.LocalInitiator.Record(), + } + + stream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + return stream.Encode(w) +} + +// DecodeShutdownInfo constructs a ShutdownInfo struct by decoding the given +// byte slice. +func DecodeShutdownInfo(b []byte) (*ShutdownInfo, error) { + tlvStream := lnwire.ExtraOpaqueData(b) + + var info ShutdownInfo + records := []tlv.RecordProducer{ + &info.DeliveryScript, + &info.LocalInitiator, + } + + _, err := tlvStream.ExtractRecords(records...) + + return &info, err +} From d18565a87ed049de7e6c4ecb529cd704fcf8b79e Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:34:11 -0300 Subject: [PATCH 07/61] chanstate: move data loss kv helpers Move the data-loss commit point key and channel-bucket put/fetch helpers into chanstate. The channeldb methods still own the transaction and status mutation, but the backend-specific record handling now lives with the channel state package. --- channeldb/channel.go | 26 +++++-------------- chanstate/kv_open_channel.go | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 chanstate/kv_open_channel.go diff --git a/channeldb/channel.go b/channeldb/channel.go index c997fb0b62..d990d27ce4 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -109,10 +109,6 @@ var ( // preimage producer and their preimage store. revocationStateKey = []byte("revocation-state-key") - // dataLossCommitPointKey stores the commitment point received from the - // remote peer during a channel sync in case we have lost channel state. - dataLossCommitPointKey = []byte("data-loss-commit-point-key") - // forceCloseTxKey points to a the unilateral closing tx that we // broadcasted when moving the channel to state CommitBroadcasted. forceCloseTxKey = []byte("closing-tx-key") @@ -1043,13 +1039,10 @@ func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, commitPoint *btcec.PublicKey) error { - var b bytes.Buffer - if err := WriteElement(&b, commitPoint); err != nil { - return err - } - putCommitPoint := func(chanBucket kvdb.RwBucket) error { - return chanBucket.Put(dataLossCommitPointKey, b.Bytes()) + return cstate.PutChannelDataLossCommitPoint( + chanBucket, commitPoint, + ) } return c.putChanStatus(channel, ChanStatusLocalDataLoss, putCommitPoint) @@ -1075,16 +1068,11 @@ func (c *ChannelStateDB) FetchChannelDataLossCommitPoint( return err } - bs := chanBucket.Get(dataLossCommitPointKey) - if bs == nil { - return ErrNoCommitPoint - } - r := bytes.NewReader(bs) - if err := ReadElements(r, &commitPoint); err != nil { - return err - } + commitPoint, err = cstate.FetchChannelDataLossCommitPoint( + chanBucket, + ) - return nil + return err }, func() { commitPoint = nil }) diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go new file mode 100644 index 0000000000..c401fd6b9c --- /dev/null +++ b/chanstate/kv_open_channel.go @@ -0,0 +1,50 @@ +package chanstate + +import ( + "bytes" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + // dataLossCommitPointKey stores the commitment point received from the + // remote peer during a channel sync in case we have lost channel state. + dataLossCommitPointKey = []byte("data-loss-commit-point-key") +) + +// DataLossCommitPointKey returns the key used to store the data-loss commit +// point in a channel bucket. +func DataLossCommitPointKey() []byte { + return dataLossCommitPointKey +} + +// PutChannelDataLossCommitPoint stores the data-loss commit point in the +// target channel bucket. +func PutChannelDataLossCommitPoint(chanBucket kvdb.RwBucket, + commitPoint *btcec.PublicKey) error { + + return chanBucket.Put( + dataLossCommitPointKey, commitPoint.SerializeCompressed(), + ) +} + +// FetchChannelDataLossCommitPoint retrieves the data-loss commit point from the +// target channel bucket. +func FetchChannelDataLossCommitPoint( + chanBucket kvdb.RBucket) (*btcec.PublicKey, error) { + + bs := chanBucket.Get(dataLossCommitPointKey) + if bs == nil { + return nil, ErrNoCommitPoint + } + + var b [btcec.PubKeyBytesLenCompressed]byte + r := bytes.NewReader(bs) + if _, err := io.ReadFull(r, b[:]); err != nil { + return nil, err + } + + return btcec.ParsePubKey(b[:]) +} From ba7c9982a8a87ea5eba183dbc01dba8de157c62f Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:39:06 -0300 Subject: [PATCH 08/61] chanstate: move close tx kv helpers Move the force-close and coop-close transaction keys plus the channel-bucket put/fetch helpers into chanstate. ChannelStateDB still owns the status mutation and transaction lookup, while the backend-specific record encoding now lives with the channel state package. --- channeldb/channel.go | 32 ++++++--------------- chanstate/kv_close_tx.go | 62 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 chanstate/kv_close_tx.go diff --git a/channeldb/channel.go b/channeldb/channel.go index d990d27ce4..bfe92d1ba9 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -109,14 +109,6 @@ var ( // preimage producer and their preimage store. revocationStateKey = []byte("revocation-state-key") - // forceCloseTxKey points to a the unilateral closing tx that we - // broadcasted when moving the channel to state CommitBroadcasted. - forceCloseTxKey = []byte("closing-tx-key") - - // coopCloseTxKey points to a the cooperative closing tx that we - // broadcasted when moving the channel to state CoopBroadcasted. - coopCloseTxKey = []byte("coop-closing-tx-key") - // commitDiffKey stores the current pending commitment state we've // extended to the remote party (if any). Each time we propose a new // state, we store the information necessary to reconstruct this state @@ -1175,7 +1167,7 @@ func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( closer lntypes.ChannelParty) error { return c.markBroadcasted( - channel, ChanStatusCommitBroadcasted, forceCloseTxKey, + channel, ChanStatusCommitBroadcasted, cstate.ForceCloseTxKey(), closeTx, closer, ) } @@ -1186,7 +1178,7 @@ func (c *ChannelStateDB) MarkChannelCoopBroadcasted(channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { return c.markBroadcasted( - channel, ChanStatusCoopBroadcasted, coopCloseTxKey, + channel, ChanStatusCoopBroadcasted, cstate.CoopCloseTxKey(), closeTx, closer, ) } @@ -1205,13 +1197,8 @@ func (c *ChannelStateDB) markBroadcasted(channel *OpenChannel, channel.Lock() defer channel.Unlock() - var b bytes.Buffer - if err := WriteElement(&b, closeTx); err != nil { - return err - } - putClosingTx := func(chanBucket kvdb.RwBucket) error { - return chanBucket.Put(key, b.Bytes()) + return cstate.PutChannelCloseTx(chanBucket, key, closeTx) } // Add the initiator status to the status provided. These statuses are @@ -1231,7 +1218,7 @@ func (c *ChannelStateDB) markBroadcasted(channel *OpenChannel, func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( channel *OpenChannel) (*wire.MsgTx, error) { - return c.getClosingTx(channel, forceCloseTxKey) + return c.getClosingTx(channel, cstate.ForceCloseTxKey()) } // FetchChannelBroadcastedCooperative fetches the stored cooperative closing @@ -1239,7 +1226,7 @@ func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( func (c *ChannelStateDB) FetchChannelBroadcastedCooperative( channel *OpenChannel) (*wire.MsgTx, error) { - return c.getClosingTx(channel, coopCloseTxKey) + return c.getClosingTx(channel, cstate.CoopCloseTxKey()) } // getClosingTx returns the stored closing transaction for key. The caller @@ -1262,12 +1249,9 @@ func (c *ChannelStateDB) getClosingTx(channel *OpenChannel, return err } - bs := chanBucket.Get(key) - if bs == nil { - return ErrNoCloseTx - } - r := bytes.NewReader(bs) - return ReadElement(r, &closeTx) + closeTx, err = cstate.FetchChannelCloseTx(chanBucket, key) + + return err }, func() { closeTx = nil }) diff --git a/chanstate/kv_close_tx.go b/chanstate/kv_close_tx.go new file mode 100644 index 0000000000..9687ae9d8f --- /dev/null +++ b/chanstate/kv_close_tx.go @@ -0,0 +1,62 @@ +package chanstate + +import ( + "bytes" + + "github.com/btcsuite/btcd/wire/v2" + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + // forceCloseTxKey points to a the unilateral closing tx that we + // broadcasted when moving the channel to state CommitBroadcasted. + forceCloseTxKey = []byte("closing-tx-key") + + // coopCloseTxKey points to a the cooperative closing tx that we + // broadcasted when moving the channel to state CoopBroadcasted. + coopCloseTxKey = []byte("coop-closing-tx-key") +) + +// ForceCloseTxKey returns the key used to store the unilateral closing +// transaction in a channel bucket. +func ForceCloseTxKey() []byte { + return forceCloseTxKey +} + +// CoopCloseTxKey returns the key used to store the cooperative closing +// transaction in a channel bucket. +func CoopCloseTxKey() []byte { + return coopCloseTxKey +} + +// PutChannelCloseTx stores the closing transaction under the requested key in +// the target channel bucket. +func PutChannelCloseTx(chanBucket kvdb.RwBucket, key []byte, + closeTx *wire.MsgTx) error { + + var b bytes.Buffer + if err := closeTx.Serialize(&b); err != nil { + return err + } + + return chanBucket.Put(key, b.Bytes()) +} + +// FetchChannelCloseTx retrieves the closing transaction stored under the +// requested key in the target channel bucket. +func FetchChannelCloseTx(chanBucket kvdb.RBucket, + key []byte) (*wire.MsgTx, error) { + + bs := chanBucket.Get(key) + if bs == nil { + return nil, ErrNoCloseTx + } + + closeTx := wire.NewMsgTx(2) + r := bytes.NewReader(bs) + if err := closeTx.Deserialize(r); err != nil { + return nil, err + } + + return closeTx, nil +} From c7e810f69f1f5f8e3071346bbb76290af44cdfd9 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:43:18 -0300 Subject: [PATCH 09/61] chanstate: move channel setup kv helpers Move the temporary channel-opening state bucket and transaction helpers into chanstate. Keep channeldb as the compatibility surface and alias ErrChannelNotFound from chanstate so existing callers keep the same error name while setup-state storage moves packages. --- channeldb/db.go | 40 ++++++----------------- channeldb/error.go | 4 ++- chanstate/errors.go | 4 +++ chanstate/kv_channel_setup.go | 61 +++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 chanstate/kv_channel_setup.go diff --git a/channeldb/db.go b/channeldb/db.go index 86fae93b3a..ab8e0b01d2 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -342,11 +342,6 @@ var ( // Big endian is the preferred byte order, due to cursor scans over // integer keys iterating in order. byteOrder = binary.BigEndian - - // channelOpeningStateBucket is the database bucket used to store the - // channelOpeningState for each channel that is currently in the process - // of being opened. - channelOpeningStateBucket = []byte("channelOpeningState") ) // DB is the primary datastore for the lnd daemon. The database stores @@ -1798,12 +1793,9 @@ func (c *ChannelStateDB) SaveChannelOpeningState(outPoint, serializedState []byte) error { return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - bucket, err := tx.CreateTopLevelBucket(channelOpeningStateBucket) - if err != nil { - return err - } - - return bucket.Put(outPoint, serializedState) + return chanstate.SaveChannelOpeningState( + tx, outPoint, serializedState, + ) }, func() {}) } @@ -1815,21 +1807,12 @@ func (c *ChannelStateDB) GetChannelOpeningState(outPoint []byte) ([]byte, var serializedState []byte err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - bucket := tx.ReadBucket(channelOpeningStateBucket) - if bucket == nil { - // If the bucket does not exist, it means we never added - // a channel to the db, so return ErrChannelNotFound. - return ErrChannelNotFound - } - - stateBytes := bucket.Get(outPoint) - if stateBytes == nil { - return ErrChannelNotFound - } - - serializedState = append(serializedState, stateBytes...) + var err error + serializedState, err = chanstate.GetChannelOpeningState( + tx, outPoint, + ) - return nil + return err }, func() { serializedState = nil }) @@ -1839,12 +1822,7 @@ func (c *ChannelStateDB) GetChannelOpeningState(outPoint []byte) ([]byte, // DeleteChannelOpeningState removes any state for outPoint from the database. func (c *ChannelStateDB) DeleteChannelOpeningState(outPoint []byte) error { return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - bucket := tx.ReadWriteBucket(channelOpeningStateBucket) - if bucket == nil { - return ErrChannelNotFound - } - - return bucket.Delete(outPoint) + return chanstate.DeleteChannelOpeningState(tx, outPoint) }, func() {}) } diff --git a/channeldb/error.go b/channeldb/error.go index c2b2dde0d7..1fbca916e8 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -2,6 +2,8 @@ package channeldb import ( "fmt" + + cstate "github.com/lightningnetwork/lnd/chanstate" ) var ( @@ -36,7 +38,7 @@ var ( // ErrChannelNotFound is returned when we attempt to locate a channel // for a specific chain, but it is not found. - ErrChannelNotFound = fmt.Errorf("channel not found") + ErrChannelNotFound = cstate.ErrChannelNotFound // ErrMetaNotFound is returned when meta bucket hasn't been // created. diff --git a/chanstate/errors.go b/chanstate/errors.go index 4e8415cf95..ba203c9825 100644 --- a/chanstate/errors.go +++ b/chanstate/errors.go @@ -14,6 +14,10 @@ var ( // have any channels state. ErrNoChanInfoFound = fmt.Errorf("no chan info found") + // ErrChannelNotFound is returned when we attempt to locate a channel + // for a specific chain, but it is not found. + ErrChannelNotFound = fmt.Errorf("channel not found") + // ErrNoRevocationsFound is returned when revocation state for a // particular channel cannot be found. ErrNoRevocationsFound = fmt.Errorf("no revocations found") diff --git a/chanstate/kv_channel_setup.go b/chanstate/kv_channel_setup.go new file mode 100644 index 0000000000..555444b9ae --- /dev/null +++ b/chanstate/kv_channel_setup.go @@ -0,0 +1,61 @@ +package chanstate + +import "github.com/lightningnetwork/lnd/kvdb" + +var ( + // channelOpeningStateBucket is the database bucket used to store the + // channelOpeningState for each channel that is currently in the process + // of being opened. + channelOpeningStateBucket = []byte("channelOpeningState") +) + +// ChannelOpeningStateBucketKey returns the top-level bucket key used to store +// serialized channel opening state. +func ChannelOpeningStateBucketKey() []byte { + return channelOpeningStateBucket +} + +// SaveChannelOpeningState saves the serialized channel state for the provided +// chanPoint to the channelOpeningStateBucket. +func SaveChannelOpeningState(tx kvdb.RwTx, outPoint, + serializedState []byte) error { + + bucket, err := tx.CreateTopLevelBucket(channelOpeningStateBucket) + if err != nil { + return err + } + + return bucket.Put(outPoint, serializedState) +} + +// GetChannelOpeningState fetches the serialized channel state for the provided +// outPoint from the database, or returns ErrChannelNotFound if the channel is +// not found. +func GetChannelOpeningState(tx kvdb.RTx, outPoint []byte) ([]byte, error) { + bucket := tx.ReadBucket(channelOpeningStateBucket) + if bucket == nil { + // If the bucket does not exist, it means we never added + // a channel to the db, so return ErrChannelNotFound. + return nil, ErrChannelNotFound + } + + stateBytes := bucket.Get(outPoint) + if stateBytes == nil { + return nil, ErrChannelNotFound + } + + var serializedState []byte + serializedState = append(serializedState, stateBytes...) + + return serializedState, nil +} + +// DeleteChannelOpeningState removes any state for outPoint from the database. +func DeleteChannelOpeningState(tx kvdb.RwTx, outPoint []byte) error { + bucket := tx.ReadWriteBucket(channelOpeningStateBucket) + if bucket == nil { + return ErrChannelNotFound + } + + return bucket.Delete(outPoint) +} From 8d8e0b9d0f456f0cd69782168b2ebddf772c4c2e Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:46:15 -0300 Subject: [PATCH 10/61] chanstate: move thaw height kv helpers Move the frozen-channel thaw height key and channel-bucket fetch/store/delete helpers into chanstate. The channeldb functions remain as compatibility wrappers while the larger open-channel storage code is still being moved incrementally. --- channeldb/channel.go | 25 +++---------------- chanstate/kv_thaw_height.go | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 chanstate/kv_thaw_height.go diff --git a/channeldb/channel.go b/channeldb/channel.go index bfe92d1ba9..b7195d26ef 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -118,11 +118,6 @@ var ( // TODO(roasbeef): rename to commit chain? commitDiffKey = []byte("commit-diff-key") - // frozenChanKey is the key where we store the information for any - // active "frozen" channels. This key is present only in the leaf - // bucket for a given channel. - frozenChanKey = []byte("frozen-chans") - // lastWasRevokeKey is a key that stores true when the last update we // sent was a revocation and false when it was a commitment signature. // This is nil in the case of new channels with no updates exchanged. @@ -3424,29 +3419,15 @@ func makeLogKey(updateNum uint64) [8]byte { } func fetchThawHeight(chanBucket kvdb.RBucket) (uint32, error) { - var height uint32 - - heightBytes := chanBucket.Get(frozenChanKey) - heightReader := bytes.NewReader(heightBytes) - - if err := ReadElements(heightReader, &height); err != nil { - return 0, err - } - - return height, nil + return cstate.FetchThawHeight(chanBucket) } func storeThawHeight(chanBucket kvdb.RwBucket, height uint32) error { - var heightBuf bytes.Buffer - if err := WriteElements(&heightBuf, height); err != nil { - return err - } - - return chanBucket.Put(frozenChanKey, heightBuf.Bytes()) + return cstate.StoreThawHeight(chanBucket, height) } func deleteThawHeight(chanBucket kvdb.RwBucket) error { - return chanBucket.Delete(frozenChanKey) + return cstate.DeleteThawHeight(chanBucket) } // keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the diff --git a/chanstate/kv_thaw_height.go b/chanstate/kv_thaw_height.go new file mode 100644 index 0000000000..64b4991534 --- /dev/null +++ b/chanstate/kv_thaw_height.go @@ -0,0 +1,49 @@ +package chanstate + +import ( + "bytes" + "encoding/binary" + + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + // frozenChanKey is the key where we store the information for any + // active "frozen" channels. This key is present only in the leaf + // bucket for a given channel. + frozenChanKey = []byte("frozen-chans") +) + +// FrozenChanKey returns the key used to store a channel's thaw height. +func FrozenChanKey() []byte { + return frozenChanKey +} + +// FetchThawHeight fetches a channel's thaw height from the channel bucket. +func FetchThawHeight(chanBucket kvdb.RBucket) (uint32, error) { + var height uint32 + + heightBytes := chanBucket.Get(frozenChanKey) + heightReader := bytes.NewReader(heightBytes) + + if err := binary.Read(heightReader, byteOrder, &height); err != nil { + return 0, err + } + + return height, nil +} + +// StoreThawHeight stores a channel's thaw height in the channel bucket. +func StoreThawHeight(chanBucket kvdb.RwBucket, height uint32) error { + var heightBuf bytes.Buffer + if err := binary.Write(&heightBuf, byteOrder, height); err != nil { + return err + } + + return chanBucket.Put(frozenChanKey, heightBuf.Bytes()) +} + +// DeleteThawHeight deletes a channel's thaw height from the channel bucket. +func DeleteThawHeight(chanBucket kvdb.RwBucket) error { + return chanBucket.Delete(frozenChanKey) +} From 49c2ede71d5d759e9432edb5c8f18b89f895ed3a Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 22:53:22 -0300 Subject: [PATCH 11/61] chanstate: move channel kv keys Move the live channel-state bucket and record key definitions into chanstate while keeping channeldb aliases for the current callers. This keeps the on-disk layout documentation next to the channel-state package before the larger open-channel serialization code is moved. --- channeldb/channel.go | 99 +++----------------- chanstate/kv_channel_keys.go | 169 +++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 85 deletions(-) create mode 100644 chanstate/kv_channel_keys.go diff --git a/channeldb/channel.go b/channeldb/channel.go index b7195d26ef..10e8d684a9 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -37,91 +37,20 @@ const ( ) var ( - // closedChannelBucket stores summarization information concerning - // previously open, but now closed channels. - closedChannelBucket = []byte("closed-chan-bucket") - - // openChannelBucket stores all the currently open channels. This bucket - // has a second, nested bucket which is keyed by a node's ID. Within - // that node ID bucket, all attributes required to track, update, and - // close a channel are stored. - // - // openChan -> nodeID -> chanPoint - // - // TODO(roasbeef): flesh out comment - openChannelBucket = []byte("open-chan-bucket") - - // outpointBucket stores all of our channel outpoints and a tlv - // stream containing channel data. - // - // outpoint -> tlv stream. - // - outpointBucket = []byte("outpoint-bucket") - - // chanIDBucket stores all of the 32-byte channel ID's we know about. - // These could be derived from outpointBucket, but it is more - // convenient to have these in their own bucket. - // - // chanID -> tlv stream. - // - chanIDBucket = []byte("chan-id-bucket") - - // historicalChannelBucket stores all channels that have seen their - // commitment tx confirm. All information from their previous open state - // is retained. - historicalChannelBucket = []byte("historical-chan-bucket") - - // chanInfoKey can be accessed within the bucket for a channel - // (identified by its chanPoint). This key stores all the static - // information for a channel which is decided at the end of the - // funding flow. - chanInfoKey = []byte("chan-info-key") - - // localUpfrontShutdownKey can be accessed within the bucket for a channel - // (identified by its chanPoint). This key stores an optional upfront - // shutdown script for the local peer. - localUpfrontShutdownKey = []byte("local-upfront-shutdown-key") - - // remoteUpfrontShutdownKey can be accessed within the bucket for a channel - // (identified by its chanPoint). This key stores an optional upfront - // shutdown script for the remote peer. - remoteUpfrontShutdownKey = []byte("remote-upfront-shutdown-key") - - // chanCommitmentKey can be accessed within the sub-bucket for a - // particular channel. This key stores the up to date commitment state - // for a particular channel party. Appending a 0 to the end of this key - // indicates it's the commitment for the local party, and appending a 1 - // to the end of this key indicates it's the commitment for the remote - // party. - chanCommitmentKey = []byte("chan-commitment-key") - - // unsignedAckedUpdatesKey is an entry in the channel bucket that - // contains the remote updates that we have acked, but not yet signed - // for in one of our remote commits. - unsignedAckedUpdatesKey = []byte("unsigned-acked-updates-key") - - // remoteUnsignedLocalUpdatesKey is an entry in the channel bucket that - // contains the local updates that the remote party has acked, but - // has not yet signed for in one of their local commits. - remoteUnsignedLocalUpdatesKey = []byte("remote-unsigned-local-updates-key") - - // revocationStateKey stores their current revocation hash, our - // preimage producer and their preimage store. - revocationStateKey = []byte("revocation-state-key") - - // commitDiffKey stores the current pending commitment state we've - // extended to the remote party (if any). Each time we propose a new - // state, we store the information necessary to reconstruct this state - // from the prior commitment. This allows us to resync the remote party - // to their expected state in the case of message loss. - // - // TODO(roasbeef): rename to commit chain? - commitDiffKey = []byte("commit-diff-key") - - // lastWasRevokeKey is a key that stores true when the last update we - // sent was a revocation and false when it was a commitment signature. - // This is nil in the case of new channels with no updates exchanged. - lastWasRevokeKey = []byte("last-was-revoke") + closedChannelBucket = cstate.ClosedChannelBucketKey() + openChannelBucket = cstate.OpenChannelBucketKey() + outpointBucket = cstate.OutpointBucketKey() + chanIDBucket = cstate.ChanIDBucketKey() + historicalChannelBucket = cstate.HistoricalChannelBucketKey() + chanInfoKey = cstate.ChanInfoKey() + localUpfrontShutdownKey = cstate.LocalUpfrontShutdownKey() + remoteUpfrontShutdownKey = cstate.RemoteUpfrontShutdownKey() + chanCommitmentKey = cstate.ChanCommitmentKey() + unsignedAckedUpdatesKey = cstate.UnsignedAckedUpdatesKey() + remoteUnsignedLocalUpdatesKey = cstate.RemoteUnsignedLocalUpdatesKey() + revocationStateKey = cstate.RevocationStateKey() + commitDiffKey = cstate.CommitDiffKey() + lastWasRevokeKey = cstate.LastWasRevokeKey() ) var ( diff --git a/chanstate/kv_channel_keys.go b/chanstate/kv_channel_keys.go new file mode 100644 index 0000000000..502f450c4c --- /dev/null +++ b/chanstate/kv_channel_keys.go @@ -0,0 +1,169 @@ +package chanstate + +var ( + // closedChannelBucket stores summarization information concerning + // previously open, but now closed channels. + closedChannelBucket = []byte("closed-chan-bucket") + + // openChannelBucket stores all the currently open channels. This bucket + // has a second, nested bucket which is keyed by a node's ID. Within + // that node ID bucket, all attributes required to track, update, and + // close a channel are stored. + // + // openChan -> nodeID -> chanPoint + // + // TODO(roasbeef): flesh out comment. + openChannelBucket = []byte("open-chan-bucket") + + // outpointBucket stores all of our channel outpoints and a tlv + // stream containing channel data. + // + // outpoint -> tlv stream. + // + outpointBucket = []byte("outpoint-bucket") + + // chanIDBucket stores all of the 32-byte channel ID's we know about. + // These could be derived from outpointBucket, but it is more + // convenient to have these in their own bucket. + // + // chanID -> tlv stream. + // + chanIDBucket = []byte("chan-id-bucket") + + // historicalChannelBucket stores all channels that have seen their + // commitment tx confirm. All information from their previous open state + // is retained. + historicalChannelBucket = []byte("historical-chan-bucket") + + // chanInfoKey can be accessed within the bucket for a channel + // (identified by its chanPoint). This key stores all the static + // information for a channel which is decided at the end of the + // funding flow. + chanInfoKey = []byte("chan-info-key") + + // localUpfrontShutdownKey can be accessed within the bucket for a + // channel (identified by its chanPoint). This key stores an optional + // upfront shutdown script for the local peer. + localUpfrontShutdownKey = []byte("local-upfront-shutdown-key") + + // remoteUpfrontShutdownKey can be accessed within the bucket for a + // channel (identified by its chanPoint). This key stores an optional + // upfront shutdown script for the remote peer. + remoteUpfrontShutdownKey = []byte("remote-upfront-shutdown-key") + + // chanCommitmentKey can be accessed within the sub-bucket for a + // particular channel. This key stores the up to date commitment state + // for a particular channel party. Appending a 0 to the end of this key + // indicates it's the commitment for the local party, and appending a 1 + // to the end of this key indicates it's the commitment for the remote + // party. + chanCommitmentKey = []byte("chan-commitment-key") + + // unsignedAckedUpdatesKey is an entry in the channel bucket that + // contains the remote updates that we have acked, but not yet signed + // for in one of our remote commits. + unsignedAckedUpdatesKey = []byte("unsigned-acked-updates-key") + + // remoteUnsignedLocalUpdatesKey is an entry in the channel bucket that + // contains the local updates that the remote party has acked, but + // has not yet signed for in one of their local commits. + remoteUnsignedLocalUpdatesKey = []byte( + "remote-unsigned-local-updates-key", + ) + + // revocationStateKey stores their current revocation hash, our + // preimage producer and their preimage store. + revocationStateKey = []byte("revocation-state-key") + + // commitDiffKey stores the current pending commitment state we've + // extended to the remote party (if any). Each time we propose a new + // state, we store the information necessary to reconstruct this state + // from the prior commitment. This allows us to resync the remote party + // to their expected state in the case of message loss. + // + // TODO(roasbeef): rename to commit chain? + commitDiffKey = []byte("commit-diff-key") + + // lastWasRevokeKey is a key that stores true when the last update we + // sent was a revocation and false when it was a commitment signature. + // This is nil in the case of new channels with no updates exchanged. + lastWasRevokeKey = []byte("last-was-revoke") +) + +// ClosedChannelBucketKey returns the top-level closed-channel summary bucket +// key. +func ClosedChannelBucketKey() []byte { + return closedChannelBucket +} + +// OpenChannelBucketKey returns the top-level open-channel bucket key. +func OpenChannelBucketKey() []byte { + return openChannelBucket +} + +// OutpointBucketKey returns the top-level outpoint index bucket key. +func OutpointBucketKey() []byte { + return outpointBucket +} + +// ChanIDBucketKey returns the top-level channel ID index bucket key. +func ChanIDBucketKey() []byte { + return chanIDBucket +} + +// HistoricalChannelBucketKey returns the top-level historical channel bucket +// key. +func HistoricalChannelBucketKey() []byte { + return historicalChannelBucket +} + +// ChanInfoKey returns the channel-bucket key for static channel information. +func ChanInfoKey() []byte { + return chanInfoKey +} + +// LocalUpfrontShutdownKey returns the channel-bucket key for the local upfront +// shutdown script. +func LocalUpfrontShutdownKey() []byte { + return localUpfrontShutdownKey +} + +// RemoteUpfrontShutdownKey returns the channel-bucket key for the remote +// upfront shutdown script. +func RemoteUpfrontShutdownKey() []byte { + return remoteUpfrontShutdownKey +} + +// ChanCommitmentKey returns the channel-bucket key prefix for channel +// commitments. +func ChanCommitmentKey() []byte { + return chanCommitmentKey +} + +// UnsignedAckedUpdatesKey returns the channel-bucket key for unsigned acked +// remote updates. +func UnsignedAckedUpdatesKey() []byte { + return unsignedAckedUpdatesKey +} + +// RemoteUnsignedLocalUpdatesKey returns the channel-bucket key for remote +// unsigned local updates. +func RemoteUnsignedLocalUpdatesKey() []byte { + return remoteUnsignedLocalUpdatesKey +} + +// RevocationStateKey returns the channel-bucket key for revocation state. +func RevocationStateKey() []byte { + return revocationStateKey +} + +// CommitDiffKey returns the channel-bucket key for the current pending +// commitment diff. +func CommitDiffKey() []byte { + return commitDiffKey +} + +// LastWasRevokeKey returns the channel-bucket key for the last update type. +func LastWasRevokeKey() []byte { + return lastWasRevokeKey +} From 450e63e050d40ceb9d5039f2281163f02b272fa6 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:00:33 -0300 Subject: [PATCH 12/61] chanstate: move open channel bucket helpers Move the open-channel bucket lookup helpers and outpoint-closed reader into chanstate. Keep channeldb wrappers for the current call sites, and alias the related lookup errors back from chanstate while the larger channel storage code continues to move incrementally. --- channeldb/channel.go | 132 ++------------------------- channeldb/error.go | 4 +- chanstate/errors.go | 8 ++ chanstate/kv_open_channel.go | 171 +++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 125 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 10e8d684a9..6677e1480a 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -276,26 +276,7 @@ const ( // 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 + return cstate.IsOutpointClosed(opBucket, chanKey) } // ChannelType is an enum-like type that describes one of several possible @@ -605,56 +586,7 @@ func (c *ChannelStateDB) RefreshChannel(channel *OpenChannel) error { func fetchChanBucket(tx kvdb.RTx, nodeKey *btcec.PublicKey, outPoint *wire.OutPoint, chainHash chainhash.Hash) (kvdb.RBucket, error) { - // First fetch the top level bucket which stores all data related to - // current, active channels. - openChanBucket := tx.ReadBucket(openChannelBucket) - if openChanBucket == nil { - return nil, ErrNoChanDBExists - } - - // TODO(roasbeef): CreateTopLevelBucket on the interface isn't like - // CreateIfNotExists, will return error - - // Within this top level bucket, fetch the bucket dedicated to storing - // open channel data specific to the remote node. - nodePub := nodeKey.SerializeCompressed() - nodeChanBucket := openChanBucket.NestedReadBucket(nodePub) - if nodeChanBucket == nil { - return nil, ErrNoActiveChannels - } - - // We'll then recurse down an additional layer in order to fetch the - // bucket for this particular chain. - chainBucket := nodeChanBucket.NestedReadBucket(chainHash[:]) - if chainBucket == nil { - return nil, ErrNoActiveChannels - } - - // With the bucket for the node and chain fetched, we can now go down - // another level, for this channel itself. - var chanPointBuf bytes.Buffer - if err := graphdb.WriteOutpoint(&chanPointBuf, outPoint); err != nil { - return nil, err - } - chanKey := chanPointBuf.Bytes() - - // Treat already-closed channels as gone. The chanBucket may still - // exist on tombstone-enabled backends; the outpoint flip is the - // source of truth. - closed, err := isOutpointClosed(tx.ReadBucket(outpointBucket), chanKey) - if err != nil { - return nil, err - } - if closed { - return nil, ErrChannelNotFound - } - - chanBucket := chainBucket.NestedReadBucket(chanKey) - if chanBucket == nil { - return nil, ErrChannelNotFound - } - - return chanBucket, nil + return cstate.FetchChanBucket(tx, nodeKey, outPoint, chainHash) } // fetchChanBucketRw is a helper function that returns the bucket where a @@ -665,56 +597,7 @@ func fetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey, outPoint *wire.OutPoint, chainHash chainhash.Hash) (kvdb.RwBucket, error) { - // First fetch the top level bucket which stores all data related to - // current, active channels. - openChanBucket := tx.ReadWriteBucket(openChannelBucket) - if openChanBucket == nil { - return nil, ErrNoChanDBExists - } - - // TODO(roasbeef): CreateTopLevelBucket on the interface isn't like - // CreateIfNotExists, will return error - - // Within this top level bucket, fetch the bucket dedicated to storing - // open channel data specific to the remote node. - nodePub := nodeKey.SerializeCompressed() - nodeChanBucket := openChanBucket.NestedReadWriteBucket(nodePub) - if nodeChanBucket == nil { - return nil, ErrNoActiveChannels - } - - // We'll then recurse down an additional layer in order to fetch the - // bucket for this particular chain. - chainBucket := nodeChanBucket.NestedReadWriteBucket(chainHash[:]) - if chainBucket == nil { - return nil, ErrNoActiveChannels - } - - // With the bucket for the node and chain fetched, we can now go down - // another level, for this channel itself. - var chanPointBuf bytes.Buffer - if err := graphdb.WriteOutpoint(&chanPointBuf, outPoint); err != nil { - return nil, err - } - chanKey := chanPointBuf.Bytes() - - // Treat already-closed channels as gone. The chanBucket may still - // exist on tombstone-enabled backends; the outpoint flip is the - // source of truth. - closed, err := isOutpointClosed(tx.ReadBucket(outpointBucket), chanKey) - if err != nil { - return nil, err - } - if closed { - return nil, ErrChannelNotFound - } - - chanBucket := chainBucket.NestedReadWriteBucket(chanKey) - if chanBucket == nil { - return nil, ErrChannelNotFound - } - - return chanBucket, nil + return cstate.FetchChanBucketRw(tx, nodeKey, outPoint, chainHash) } func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, @@ -976,9 +859,12 @@ func (c *ChannelStateDB) FetchChannelDataLossCommitPoint( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, ) - switch err { - case nil: - case ErrNoChanDBExists, ErrNoActiveChannels, ErrChannelNotFound: + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + return ErrNoCommitPoint default: return err diff --git a/channeldb/error.go b/channeldb/error.go index 1fbca916e8..d55be66cb6 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -9,7 +9,7 @@ import ( var ( // ErrNoChanDBExists is returned when a channel bucket hasn't been // created. - ErrNoChanDBExists = fmt.Errorf("channel db has not yet been created") + ErrNoChanDBExists = cstate.ErrNoChanDBExists // ErrNoHistoricalBucket is returned when the historical channel bucket // not been created yet. @@ -26,7 +26,7 @@ var ( // ErrNoActiveChannels is returned when there is no active (open) // channels within the database. - ErrNoActiveChannels = fmt.Errorf("no active channels exist") + ErrNoActiveChannels = cstate.ErrNoActiveChannels // ErrNoPastDeltas is returned when the channel delta bucket hasn't been // created. diff --git a/chanstate/errors.go b/chanstate/errors.go index ba203c9825..54e4855470 100644 --- a/chanstate/errors.go +++ b/chanstate/errors.go @@ -6,6 +6,10 @@ import ( ) var ( + // ErrNoChanDBExists is returned when a channel bucket hasn't been + // created. + ErrNoChanDBExists = fmt.Errorf("channel db has not yet been created") + // ErrNoCommitmentsFound is returned when a channel has not set // commitment states. ErrNoCommitmentsFound = fmt.Errorf("no commitments found") @@ -28,6 +32,10 @@ var ( // tolerant. ErrNoPendingCommit = fmt.Errorf("no pending commits found") + // ErrNoActiveChannels is returned when there is no active (open) + // channels within the database. + ErrNoActiveChannels = fmt.Errorf("no active channels exist") + // ErrNoCommitPoint is returned when no data loss commit point is found // in the database. ErrNoCommitPoint = fmt.Errorf("no commit point found") diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index c401fd6b9c..d3b41ab90e 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -2,10 +2,15 @@ package chanstate import ( "bytes" + "fmt" "io" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/tlv" ) var ( @@ -48,3 +53,169 @@ func FetchChannelDataLossCommitPoint( return btcec.ParsePubKey(b[:]) } + +const ( + // A tlv type definition used to serialize an outpoint's indexStatus + // for use in the outpoint index. + indexStatusType tlv.Type = 0 +) + +// indexStatus is an enum-like type that describes what state the outpoint is +// in. Currently only two possible values. +type indexStatus uint8 + +const ( + // outpointClosed represents an outpoint that is closed in the outpoint + // index. + 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 +} + +// FetchChanBucket is a helper function that returns the bucket where a +// channel's data resides in given: the public key for the node, the outpoint, +// and the chainhash that the channel resides on. +func FetchChanBucket(tx kvdb.RTx, nodeKey *btcec.PublicKey, + outPoint *wire.OutPoint, chainHash chainhash.Hash) ( + kvdb.RBucket, error) { + + // First fetch the top level bucket which stores all data related to + // current, active channels. + openChanBucket := tx.ReadBucket(openChannelBucket) + if openChanBucket == nil { + return nil, ErrNoChanDBExists + } + + // TODO(roasbeef): CreateTopLevelBucket on the interface isn't like + // CreateIfNotExists, will return error. + + // Within this top level bucket, fetch the bucket dedicated to storing + // open channel data specific to the remote node. + nodePub := nodeKey.SerializeCompressed() + nodeChanBucket := openChanBucket.NestedReadBucket(nodePub) + if nodeChanBucket == nil { + return nil, ErrNoActiveChannels + } + + // We'll then recurse down an additional layer in order to fetch the + // bucket for this particular chain. + chainBucket := nodeChanBucket.NestedReadBucket(chainHash[:]) + if chainBucket == nil { + return nil, ErrNoActiveChannels + } + + // With the bucket for the node and chain fetched, we can now go down + // another level, for this channel itself. + var chanPointBuf bytes.Buffer + if err := graphdb.WriteOutpoint(&chanPointBuf, outPoint); err != nil { + return nil, err + } + chanKey := chanPointBuf.Bytes() + + // Treat already-closed channels as gone. The chanBucket may still + // exist on tombstone-enabled backends; the outpoint flip is the + // source of truth. + closed, err := IsOutpointClosed(tx.ReadBucket(outpointBucket), chanKey) + if err != nil { + return nil, err + } + if closed { + return nil, ErrChannelNotFound + } + + chanBucket := chainBucket.NestedReadBucket(chanKey) + if chanBucket == nil { + return nil, ErrChannelNotFound + } + + return chanBucket, nil +} + +// FetchChanBucketRw is a helper function that returns the bucket where a +// channel's data resides in given: the public key for the node, the outpoint, +// and the chainhash that the channel resides on. This differs from +// FetchChanBucket in that it returns a writeable bucket. +func FetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey, + outPoint *wire.OutPoint, chainHash chainhash.Hash) (kvdb.RwBucket, + error) { + + // First fetch the top level bucket which stores all data related to + // current, active channels. + openChanBucket := tx.ReadWriteBucket(openChannelBucket) + if openChanBucket == nil { + return nil, ErrNoChanDBExists + } + + // TODO(roasbeef): CreateTopLevelBucket on the interface isn't like + // CreateIfNotExists, will return error. + + // Within this top level bucket, fetch the bucket dedicated to storing + // open channel data specific to the remote node. + nodePub := nodeKey.SerializeCompressed() + nodeChanBucket := openChanBucket.NestedReadWriteBucket(nodePub) + if nodeChanBucket == nil { + return nil, ErrNoActiveChannels + } + + // We'll then recurse down an additional layer in order to fetch the + // bucket for this particular chain. + chainBucket := nodeChanBucket.NestedReadWriteBucket(chainHash[:]) + if chainBucket == nil { + return nil, ErrNoActiveChannels + } + + // With the bucket for the node and chain fetched, we can now go down + // another level, for this channel itself. + var chanPointBuf bytes.Buffer + if err := graphdb.WriteOutpoint(&chanPointBuf, outPoint); err != nil { + return nil, err + } + chanKey := chanPointBuf.Bytes() + + // Treat already-closed channels as gone. The chanBucket may still + // exist on tombstone-enabled backends; the outpoint flip is the + // source of truth. + closed, err := IsOutpointClosed(tx.ReadBucket(outpointBucket), chanKey) + if err != nil { + return nil, err + } + if closed { + return nil, ErrChannelNotFound + } + + chanBucket := chainBucket.NestedReadWriteBucket(chanKey) + if chanBucket == nil { + return nil, ErrChannelNotFound + } + + return chanBucket, nil +} From 086f6f3aaf0e317d14bfd9b927f3cde08cb7044f Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:06:41 -0300 Subject: [PATCH 13/61] chanstate: move outpoint index kv helpers Move outpoint-index status encoding and the closed-index update helper into chanstate. The channel open and close flows still live in channeldb, but their outpoint-index record handling now uses the channel-state KV helpers. --- channeldb/channel.go | 68 +++++++----------------------------- chanstate/kv_open_channel.go | 59 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 56 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 6677e1480a..31975f33c8 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -101,13 +101,9 @@ var ( ErrOnionBlobLength = cstate.ErrOnionBlobLength ) -const ( - // A tlv type definition used to serialize an outpoint's indexStatus - // for use in the outpoint index. - indexStatusType tlv.Type = 0 -) - type ( + indexStatus = cstate.IndexStatus + // OpenChannel encapsulates the persistent and dynamic state of an open // channel with a remote node. OpenChannel = cstate.OpenChannel @@ -128,6 +124,12 @@ type ( CommitDiff = cstate.CommitDiff ) +const ( + indexStatusType = cstate.IndexStatusType + outpointOpen = cstate.OutpointOpen + outpointClosed = cstate.OutpointClosed +) + // openChannelTlvData houses the new data fields that are stored for each // channel in a TLV stream within the root bucket. This is stored as a TLV // stream appended to the existing hard-coded fields in the channel's root @@ -254,19 +256,6 @@ func (c *openChannelTlvData) decode(r io.Reader) error { return nil } -// indexStatus is an enum-like type that describes what state the -// outpoint is in. Currently only two possible values. -type indexStatus uint8 - -const ( - // outpointOpen represents an outpoint that is open in the outpoint index. - outpointOpen indexStatus = 0 - - // outpointClosed represents an outpoint that is closed in the outpoint - // index. - 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 @@ -635,23 +624,10 @@ func fullSyncOpenChannel(tx kvdb.RwTx, c *OpenChannel) error { return ErrChanAlreadyExists } - status := uint8(outpointOpen) - - // Write the status of this outpoint as the first entry in a tlv - // stream. - statusRecord := tlv.MakePrimitiveRecord(indexStatusType, &status) - opStream, err := tlv.NewStream(statusRecord) - if err != nil { - return err - } - - var b bytes.Buffer - if err := opStream.Encode(&b); err != nil { - return err - } - // Add the outpoint to our outpoint index with the tlv stream. - if err := opBucket.Put(chanPointBuf.Bytes(), b.Bytes()); err != nil { + if err := cstate.PutOpenOutpointIndex( + opBucket, chanPointBuf.Bytes(), + ); err != nil { return err } @@ -2481,27 +2457,7 @@ func locateOpenChannel(tx kvdb.RwTx, channel *OpenChannel) (kvdb.RwBucket, // open to closed. The index entry must already exist; it was placed there // when the channel was opened. func updateClosedOutpointIndex(tx kvdb.RwTx, chanKey []byte) error { - opBucket := tx.ReadWriteBucket(outpointBucket) - if opBucket == nil { - return ErrNoChanDBExists - } - if opBucket.Get(chanKey) == nil { - return ErrMissingIndexEntry - } - - status := uint8(outpointClosed) - statusRecord := tlv.MakePrimitiveRecord(indexStatusType, &status) - opStream, err := tlv.NewStream(statusRecord) - if err != nil { - return err - } - - var b bytes.Buffer - if err := opStream.Encode(&b); err != nil { - return err - } - - return opBucket.Put(chanKey, b.Bytes()) + return cstate.UpdateClosedOutpointIndex(tx, chanKey) } // archiveClosedChannel writes the immutable close-time records of the diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index d3b41ab90e..a440f494a4 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -58,18 +58,77 @@ const ( // A tlv type definition used to serialize an outpoint's indexStatus // for use in the outpoint index. indexStatusType tlv.Type = 0 + + // IndexStatusType is the TLV type used to serialize an outpoint's + // indexStatus for use in the outpoint index. + IndexStatusType = indexStatusType ) // indexStatus is an enum-like type that describes what state the outpoint is // in. Currently only two possible values. type indexStatus uint8 +// IndexStatus is an enum-like type that describes what state the outpoint is +// in. Currently only two possible values. +type IndexStatus = indexStatus + const ( + // outpointOpen represents an outpoint that is open in the outpoint + // index. + outpointOpen indexStatus = 0 + + // OutpointOpen represents an outpoint that is open in the outpoint + // index. + OutpointOpen = outpointOpen + // outpointClosed represents an outpoint that is closed in the outpoint // index. outpointClosed indexStatus = 1 + + // OutpointClosed represents an outpoint that is closed in the outpoint + // index. + OutpointClosed = outpointClosed ) +func putOutpointIndexStatus(opBucket kvdb.RwBucket, chanKey []byte, + status indexStatus) error { + + statusByte := uint8(status) + statusRecord := tlv.MakePrimitiveRecord(indexStatusType, &statusByte) + opStream, err := tlv.NewStream(statusRecord) + if err != nil { + return err + } + + var b bytes.Buffer + if err := opStream.Encode(&b); err != nil { + return err + } + + return opBucket.Put(chanKey, b.Bytes()) +} + +// PutOpenOutpointIndex stores chanKey in the outpoint index as an open +// outpoint. +func PutOpenOutpointIndex(opBucket kvdb.RwBucket, chanKey []byte) error { + return putOutpointIndexStatus(opBucket, chanKey, outpointOpen) +} + +// UpdateClosedOutpointIndex flips the outpoint index entry for chanKey from +// open to closed. The index entry must already exist; it was placed there when +// the channel was opened. +func UpdateClosedOutpointIndex(tx kvdb.RwTx, chanKey []byte) error { + opBucket := tx.ReadWriteBucket(outpointBucket) + if opBucket == nil { + return ErrNoChanDBExists + } + if opBucket.Get(chanKey) == nil { + return ErrMissingIndexEntry + } + + return putOutpointIndexStatus(opBucket, chanKey, outpointClosed) +} + // 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 From e7e13e89afc4694eb231592d12091538adf8ead8 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:16:21 -0300 Subject: [PATCH 14/61] chanstate: move db codec Move the shared database element codec into the chanstate package so channel-state serialization helpers can use it without depending on channeldb. Leave compatibility wrappers in channeldb for existing callers and keep the peer flap time serialization helpers there because they are not channel-state code. --- channeldb/codec.go | 428 +---------------------------------------- chanstate/codec.go | 462 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+), 421 deletions(-) create mode 100644 chanstate/codec.go diff --git a/channeldb/codec.go b/channeldb/codec.go index a23bdd556c..fe35c63da4 100644 --- a/channeldb/codec.go +++ b/channeldb/codec.go @@ -1,41 +1,20 @@ package channeldb import ( - "bytes" - "encoding/binary" - "fmt" "io" - "net" "time" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil/v2" - "github.com/btcsuite/btcd/chainhash/v2" - "github.com/btcsuite/btcd/wire/v2" - graphdb "github.com/lightningnetwork/lnd/graph/db" - "github.com/lightningnetwork/lnd/keychain" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/shachain" - "github.com/lightningnetwork/lnd/tlv" + cstate "github.com/lightningnetwork/lnd/chanstate" ) // UnknownElementType is an error returned when the codec is unable to encode or // decode a particular type. -type UnknownElementType struct { - method string - element interface{} -} +type UnknownElementType = cstate.UnknownElementType // NewUnknownElementType creates a new UnknownElementType error from the passed // method name and element. func NewUnknownElementType(method string, el interface{}) UnknownElementType { - return UnknownElementType{method: method, element: el} -} - -// Error returns the name of the method that encountered the error, as well as -// the type that was unsupported. -func (e UnknownElementType) Error() string { - return fmt.Sprintf("Unknown type in %s: %T", e.method, e.element) + return cstate.NewUnknownElementType(method, el) } // WriteElement is a one-stop shop to write the big endian representation of @@ -43,419 +22,26 @@ func (e UnknownElementType) Error() string { // io.Writer should be backed by an appropriately sized byte slice, or be able // to dynamically expand to accommodate additional data. func WriteElement(w io.Writer, element interface{}) error { - switch e := element.(type) { - case keychain.KeyDescriptor: - if err := binary.Write(w, byteOrder, e.Family); err != nil { - return err - } - if err := binary.Write(w, byteOrder, e.Index); err != nil { - return err - } - - if e.PubKey != nil { - if err := binary.Write(w, byteOrder, true); err != nil { - return fmt.Errorf("error writing serialized "+ - "element: %w", err) - } - - return WriteElement(w, e.PubKey) - } - - return binary.Write(w, byteOrder, false) - case ChannelType: - var buf [8]byte - if err := tlv.WriteVarInt(w, uint64(e), &buf); err != nil { - return err - } - - case chainhash.Hash: - if _, err := w.Write(e[:]); err != nil { - return err - } - - case wire.OutPoint: - return graphdb.WriteOutpoint(w, &e) - - case lnwire.ShortChannelID: - if err := binary.Write(w, byteOrder, e.ToUint64()); err != nil { - return err - } - - case lnwire.ChannelID: - if _, err := w.Write(e[:]); err != nil { - return err - } - - case int64, uint64: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case uint32: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case int32: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case uint16: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case uint8: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case bool: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case btcutil.Amount: - if err := binary.Write(w, byteOrder, uint64(e)); err != nil { - return err - } - - case lnwire.MilliSatoshi: - if err := binary.Write(w, byteOrder, uint64(e)); err != nil { - return err - } - - case *btcec.PrivateKey: - b := e.Serialize() - if _, err := w.Write(b); err != nil { - return err - } - - case *btcec.PublicKey: - b := e.SerializeCompressed() - if _, err := w.Write(b); err != nil { - return err - } - - case shachain.Producer: - return e.Encode(w) - - case shachain.Store: - return e.Encode(w) - - case *wire.MsgTx: - return e.Serialize(w) - - case [32]byte: - if _, err := w.Write(e[:]); err != nil { - return err - } - - case []byte: - if err := wire.WriteVarBytes(w, 0, e); err != nil { - return err - } - - case lnwire.Message: - var msgBuf bytes.Buffer - if _, err := lnwire.WriteMessage(&msgBuf, e, 0); err != nil { - return err - } - - msgLen := uint16(len(msgBuf.Bytes())) - if err := WriteElements(w, msgLen); err != nil { - return err - } - - if _, err := w.Write(msgBuf.Bytes()); err != nil { - return err - } - - case ChannelStatus: - var buf [8]byte - if err := tlv.WriteVarInt(w, uint64(e), &buf); err != nil { - return err - } - - case ClosureType: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case lnwire.FundingFlag: - if err := binary.Write(w, byteOrder, e); err != nil { - return err - } - - case net.Addr: - if err := graphdb.SerializeAddr(w, e); err != nil { - return err - } - - case []net.Addr: - if err := WriteElement(w, uint32(len(e))); err != nil { - return err - } - - for _, addr := range e { - if err := graphdb.SerializeAddr(w, addr); err != nil { - return err - } - } - - default: - return UnknownElementType{"WriteElement", e} - } - - return nil + return cstate.WriteElement(w, element) } // WriteElements is writes each element in the elements slice to the passed // io.Writer using WriteElement. func WriteElements(w io.Writer, elements ...interface{}) error { - for _, element := range elements { - err := WriteElement(w, element) - if err != nil { - return err - } - } - return nil + return cstate.WriteElements(w, elements...) } // ReadElement is a one-stop utility function to deserialize any datastructure // encoded using the serialization format of the database. func ReadElement(r io.Reader, element interface{}) error { - switch e := element.(type) { - case *keychain.KeyDescriptor: - if err := binary.Read(r, byteOrder, &e.Family); err != nil { - return err - } - if err := binary.Read(r, byteOrder, &e.Index); err != nil { - return err - } - - var hasPubKey bool - if err := binary.Read(r, byteOrder, &hasPubKey); err != nil { - return err - } - - if hasPubKey { - return ReadElement(r, &e.PubKey) - } - - case *ChannelType: - var buf [8]byte - ctype, err := tlv.ReadVarInt(r, &buf) - if err != nil { - return err - } - - *e = ChannelType(ctype) - - case *chainhash.Hash: - if _, err := io.ReadFull(r, e[:]); err != nil { - return err - } - - case *wire.OutPoint: - return graphdb.ReadOutpoint(r, e) - - case *lnwire.ShortChannelID: - var a uint64 - if err := binary.Read(r, byteOrder, &a); err != nil { - return err - } - *e = lnwire.NewShortChanIDFromInt(a) - - case *lnwire.ChannelID: - if _, err := io.ReadFull(r, e[:]); err != nil { - return err - } - - case *int64, *uint64: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *uint32: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *int32: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *uint16: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *uint8: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *bool: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *btcutil.Amount: - var a uint64 - if err := binary.Read(r, byteOrder, &a); err != nil { - return err - } - - *e = btcutil.Amount(a) - - case *lnwire.MilliSatoshi: - var a uint64 - if err := binary.Read(r, byteOrder, &a); err != nil { - return err - } - - *e = lnwire.MilliSatoshi(a) - - case **btcec.PrivateKey: - var b [btcec.PrivKeyBytesLen]byte - if _, err := io.ReadFull(r, b[:]); err != nil { - return err - } - - priv, _ := btcec.PrivKeyFromBytes(b[:]) - *e = priv - - case **btcec.PublicKey: - var b [btcec.PubKeyBytesLenCompressed]byte - if _, err := io.ReadFull(r, b[:]); err != nil { - return err - } - - pubKey, err := btcec.ParsePubKey(b[:]) - if err != nil { - return err - } - *e = pubKey - - case *shachain.Producer: - var root [32]byte - if _, err := io.ReadFull(r, root[:]); err != nil { - return err - } - - // TODO(roasbeef): remove - producer, err := shachain.NewRevocationProducerFromBytes(root[:]) - if err != nil { - return err - } - - *e = producer - - case *shachain.Store: - store, err := shachain.NewRevocationStoreFromBytes(r) - if err != nil { - return err - } - - *e = store - - case **wire.MsgTx: - tx := wire.NewMsgTx(2) - if err := tx.Deserialize(r); err != nil { - return err - } - - *e = tx - - case *[32]byte: - if _, err := io.ReadFull(r, e[:]); err != nil { - return err - } - - case *[]byte: - bytes, err := wire.ReadVarBytes(r, 0, 66000, "[]byte") - if err != nil { - return err - } - - *e = bytes - - case *lnwire.Message: - var msgLen uint16 - if err := ReadElement(r, &msgLen); err != nil { - return err - } - - msgReader := io.LimitReader(r, int64(msgLen)) - msg, err := lnwire.ReadMessage(msgReader, 0) - if err != nil { - return err - } - - *e = msg - - case *ChannelStatus: - var buf [8]byte - status, err := tlv.ReadVarInt(r, &buf) - if err != nil { - return err - } - - *e = ChannelStatus(status) - - case *ClosureType: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *lnwire.FundingFlag: - if err := binary.Read(r, byteOrder, e); err != nil { - return err - } - - case *net.Addr: - addr, err := graphdb.DeserializeAddr(r) - if err != nil { - return err - } - *e = addr - - case *[]net.Addr: - var numAddrs uint32 - if err := ReadElement(r, &numAddrs); err != nil { - return err - } - - *e = make([]net.Addr, numAddrs) - for i := uint32(0); i < numAddrs; i++ { - addr, err := graphdb.DeserializeAddr(r) - if err != nil { - return err - } - (*e)[i] = addr - } - - default: - return UnknownElementType{"ReadElement", e} - } - - return nil + return cstate.ReadElement(r, element) } // ReadElements deserializes a variable number of elements into the passed // io.Reader, with each element being deserialized according to the ReadElement // function. func ReadElements(r io.Reader, elements ...interface{}) error { - for _, element := range elements { - err := ReadElement(r, element) - if err != nil { - return err - } - } - return nil + return cstate.ReadElements(r, elements...) } // deserializeTime deserializes time as unix nanoseconds. diff --git a/chanstate/codec.go b/chanstate/codec.go new file mode 100644 index 0000000000..36333733c0 --- /dev/null +++ b/chanstate/codec.go @@ -0,0 +1,462 @@ +package chanstate + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "net" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + graphdb "github.com/lightningnetwork/lnd/graph/db" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" +) + +// UnknownElementType is an error returned when the codec is unable to encode or +// decode a particular type. +type UnknownElementType struct { + method string + element interface{} +} + +// NewUnknownElementType creates a new UnknownElementType error from the passed +// method name and element. +func NewUnknownElementType(method string, el interface{}) UnknownElementType { + return UnknownElementType{method: method, element: el} +} + +// Error returns the name of the method that encountered the error, as well as +// the type that was unsupported. +func (e UnknownElementType) Error() string { + return fmt.Sprintf("Unknown type in %s: %T", e.method, e.element) +} + +// WriteElement is a one-stop shop to write the big endian representation of +// any element which is to be serialized for storage on disk. The passed +// io.Writer should be backed by an appropriately sized byte slice, or be able +// to dynamically expand to accommodate additional data. +func WriteElement(w io.Writer, element interface{}) error { //nolint:funlen + switch e := element.(type) { + case keychain.KeyDescriptor: + if err := binary.Write(w, byteOrder, e.Family); err != nil { + return err + } + if err := binary.Write(w, byteOrder, e.Index); err != nil { + return err + } + + if e.PubKey != nil { + if err := binary.Write(w, byteOrder, true); err != nil { + return fmt.Errorf("error writing serialized "+ + "element: %w", err) + } + + return WriteElement(w, e.PubKey) + } + + return binary.Write(w, byteOrder, false) + case ChannelType: + var buf [8]byte + if err := tlv.WriteVarInt(w, uint64(e), &buf); err != nil { + return err + } + + case chainhash.Hash: + if _, err := w.Write(e[:]); err != nil { + return err + } + + case wire.OutPoint: + return graphdb.WriteOutpoint(w, &e) + + case lnwire.ShortChannelID: + if err := binary.Write(w, byteOrder, e.ToUint64()); err != nil { + return err + } + + case lnwire.ChannelID: + if _, err := w.Write(e[:]); err != nil { + return err + } + + case int64, uint64: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case uint32: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case int32: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case uint16: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case uint8: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case bool: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case btcutil.Amount: + if err := binary.Write(w, byteOrder, uint64(e)); err != nil { + return err + } + + case lnwire.MilliSatoshi: + if err := binary.Write(w, byteOrder, uint64(e)); err != nil { + return err + } + + case *btcec.PrivateKey: + b := e.Serialize() + if _, err := w.Write(b); err != nil { + return err + } + + case *btcec.PublicKey: + b := e.SerializeCompressed() + if _, err := w.Write(b); err != nil { + return err + } + + case shachain.Producer: + return e.Encode(w) + + case shachain.Store: + return e.Encode(w) + + case *wire.MsgTx: + return e.Serialize(w) + + case [32]byte: + if _, err := w.Write(e[:]); err != nil { + return err + } + + case []byte: + if err := wire.WriteVarBytes(w, 0, e); err != nil { + return err + } + + case lnwire.Message: + var msgBuf bytes.Buffer + if _, err := lnwire.WriteMessage(&msgBuf, e, 0); err != nil { + return err + } + + msgLen := uint16(len(msgBuf.Bytes())) + if err := WriteElements(w, msgLen); err != nil { + return err + } + + if _, err := w.Write(msgBuf.Bytes()); err != nil { + return err + } + + case ChannelStatus: + var buf [8]byte + if err := tlv.WriteVarInt(w, uint64(e), &buf); err != nil { + return err + } + + case ClosureType: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case lnwire.FundingFlag: + if err := binary.Write(w, byteOrder, e); err != nil { + return err + } + + case net.Addr: + if err := graphdb.SerializeAddr(w, e); err != nil { + return err + } + + case []net.Addr: + if err := WriteElement(w, uint32(len(e))); err != nil { + return err + } + + for _, addr := range e { + if err := graphdb.SerializeAddr(w, addr); err != nil { + return err + } + } + + default: + return UnknownElementType{"WriteElement", e} + } + + return nil +} + +// WriteElements is writes each element in the elements slice to the passed +// io.Writer using WriteElement. +func WriteElements(w io.Writer, elements ...interface{}) error { + for _, element := range elements { + err := WriteElement(w, element) + if err != nil { + return err + } + } + + return nil +} + +// ReadElement is a one-stop utility function to deserialize any datastructure +// encoded using the serialization format of the database. +func ReadElement(r io.Reader, element interface{}) error { //nolint:funlen + switch e := element.(type) { + case *keychain.KeyDescriptor: + if err := binary.Read(r, byteOrder, &e.Family); err != nil { + return err + } + if err := binary.Read(r, byteOrder, &e.Index); err != nil { + return err + } + + var hasPubKey bool + if err := binary.Read(r, byteOrder, &hasPubKey); err != nil { + return err + } + + if hasPubKey { + return ReadElement(r, &e.PubKey) + } + + case *ChannelType: + var buf [8]byte + ctype, err := tlv.ReadVarInt(r, &buf) + if err != nil { + return err + } + + *e = ChannelType(ctype) + + case *chainhash.Hash: + if _, err := io.ReadFull(r, e[:]); err != nil { + return err + } + + case *wire.OutPoint: + return graphdb.ReadOutpoint(r, e) + + case *lnwire.ShortChannelID: + var a uint64 + if err := binary.Read(r, byteOrder, &a); err != nil { + return err + } + *e = lnwire.NewShortChanIDFromInt(a) + + case *lnwire.ChannelID: + if _, err := io.ReadFull(r, e[:]); err != nil { + return err + } + + case *int64, *uint64: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *uint32: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *int32: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *uint16: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *uint8: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *bool: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *btcutil.Amount: + var a uint64 + if err := binary.Read(r, byteOrder, &a); err != nil { + return err + } + + *e = btcutil.Amount(a) + + case *lnwire.MilliSatoshi: + var a uint64 + if err := binary.Read(r, byteOrder, &a); err != nil { + return err + } + + *e = lnwire.MilliSatoshi(a) + + case **btcec.PrivateKey: + var b [btcec.PrivKeyBytesLen]byte + if _, err := io.ReadFull(r, b[:]); err != nil { + return err + } + + priv, _ := btcec.PrivKeyFromBytes(b[:]) + *e = priv + + case **btcec.PublicKey: + var b [btcec.PubKeyBytesLenCompressed]byte + if _, err := io.ReadFull(r, b[:]); err != nil { + return err + } + + pubKey, err := btcec.ParsePubKey(b[:]) + if err != nil { + return err + } + *e = pubKey + + case *shachain.Producer: + var root [32]byte + if _, err := io.ReadFull(r, root[:]); err != nil { + return err + } + + // TODO(roasbeef): remove + producer, err := shachain.NewRevocationProducerFromBytes( + root[:], + ) + if err != nil { + return err + } + + *e = producer + + case *shachain.Store: + store, err := shachain.NewRevocationStoreFromBytes(r) + if err != nil { + return err + } + + *e = store + + case **wire.MsgTx: + tx := wire.NewMsgTx(2) + if err := tx.Deserialize(r); err != nil { + return err + } + + *e = tx + + case *[32]byte: + if _, err := io.ReadFull(r, e[:]); err != nil { + return err + } + + case *[]byte: + bytes, err := wire.ReadVarBytes(r, 0, 66000, "[]byte") + if err != nil { + return err + } + + *e = bytes + + case *lnwire.Message: + var msgLen uint16 + if err := ReadElement(r, &msgLen); err != nil { + return err + } + + msgReader := io.LimitReader(r, int64(msgLen)) + msg, err := lnwire.ReadMessage(msgReader, 0) + if err != nil { + return err + } + + *e = msg + + case *ChannelStatus: + var buf [8]byte + status, err := tlv.ReadVarInt(r, &buf) + if err != nil { + return err + } + + *e = ChannelStatus(status) + + case *ClosureType: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *lnwire.FundingFlag: + if err := binary.Read(r, byteOrder, e); err != nil { + return err + } + + case *net.Addr: + addr, err := graphdb.DeserializeAddr(r) + if err != nil { + return err + } + *e = addr + + case *[]net.Addr: + var numAddrs uint32 + if err := ReadElement(r, &numAddrs); err != nil { + return err + } + + *e = make([]net.Addr, numAddrs) + for i := uint32(0); i < numAddrs; i++ { + addr, err := graphdb.DeserializeAddr(r) + if err != nil { + return err + } + (*e)[i] = addr + } + + default: + return UnknownElementType{"ReadElement", e} + } + + return nil +} + +// ReadElements deserializes a variable number of elements into the passed +// io.Reader, with each element being deserialized according to the ReadElement +// function. +func ReadElements(r io.Reader, elements ...interface{}) error { + for _, element := range elements { + err := ReadElement(r, element) + if err != nil { + return err + } + } + + return nil +} From b6a1ea36c8b3ff7a734c5c85708a95135c84bb7b Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:22:08 -0300 Subject: [PATCH 15/61] chanstate: move htlc serialization Move the HTLC serialization helpers into chanstate now that the shared element codec is available there. Keep channeldb wrappers for the exported helpers so existing callers continue to compile while follow-up commits move more channel state code. --- channeldb/channel.go | 146 +----------------------- chanstate/htlc_serialization.go | 191 ++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 144 deletions(-) create mode 100644 chanstate/htlc_serialization.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 31975f33c8..50d4ed6464 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1414,64 +1414,6 @@ func processFinalHtlc(finalHtlcsBucket kvdb.RwBucket, upd LogUpdate, return cstate.ProcessFinalHtlc(finalHtlcsBucket, upd, finalHtlcs) } -// serializeHtlcExtraData encodes a TLV stream of extra data to be stored with a -// HTLC. It uses the update_add_htlc TLV types, because this is where extra -// data is passed with a HTLC. At present blinding points are the only extra -// data that we will store, and the function is a no-op if a nil blinding -// point is provided. -// -// This function MUST be called to persist all HTLC values when they are -// serialized. -func serializeHtlcExtraData(h *HTLC) error { - var records []tlv.RecordProducer - h.BlindingPoint.WhenSome(func(b tlv.RecordT[lnwire.BlindingPointTlvType, - *btcec.PublicKey]) { - - records = append(records, &b) - }) - - records, err := h.CustomRecords.ExtendRecordProducers(records) - if err != nil { - return err - } - - return h.ExtraData.PackRecords(records...) -} - -// deserializeHtlcExtraData extracts TLVs from the extra data persisted for the -// HTLC and populates values in the struct accordingly. -// -// This function MUST be called to populate the struct properly when HTLCs -// are deserialized. -func deserializeHtlcExtraData(h *HTLC) error { - if len(h.ExtraData) == 0 { - return nil - } - - blindingPoint := h.BlindingPoint.Zero() - tlvMap, err := h.ExtraData.ExtractRecords(&blindingPoint) - if err != nil { - return err - } - - if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil { - h.BlindingPoint = tlv.SomeRecordT(blindingPoint) - - // Remove the entry from the TLV map. Anything left in the map - // will be included in the custom records field. - delete(tlvMap, h.BlindingPoint.TlvType()) - } - - // Set the custom records field to the remaining TLV records. - customRecords, err := lnwire.NewCustomRecords(tlvMap) - if err != nil { - return err - } - h.CustomRecords = customRecords - - return nil -} - // SerializeHtlcs writes out the passed set of HTLC's into the passed writer // using the current default on-disk serialization format. // @@ -1487,36 +1429,7 @@ func deserializeHtlcExtraData(h *HTLC) error { // NOTE: This API is NOT stable, the on-disk format will likely change in the // future. func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error { - numHtlcs := uint16(len(htlcs)) - if err := WriteElement(b, numHtlcs); err != nil { - return err - } - - for _, htlc := range htlcs { - // Populate TLV stream for any additional fields contained - // in the TLV. - if err := serializeHtlcExtraData(&htlc); err != nil { - return err - } - - // The onion blob and hltc data are stored as a single var - // bytes blob. - onionAndExtraData := make( - []byte, lnwire.OnionPacketSize+len(htlc.ExtraData), - ) - copy(onionAndExtraData, htlc.OnionBlob[:]) - copy(onionAndExtraData[lnwire.OnionPacketSize:], htlc.ExtraData) - - if err := WriteElements(b, - htlc.Signature, htlc.RHash, htlc.Amt, htlc.RefundTimeout, - htlc.OutputIndex, htlc.Incoming, onionAndExtraData, - htlc.HtlcIndex, htlc.LogIndex, - ); err != nil { - return err - } - } - - return nil + return cstate.SerializeHtlcs(b, htlcs...) } // DeserializeHtlcs attempts to read out a slice of HTLC's from the passed @@ -1537,62 +1450,7 @@ func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error { // NOTE: This API is NOT stable, the on-disk format will likely change in the // future. func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { - var numHtlcs uint16 - if err := ReadElement(r, &numHtlcs); err != nil { - return nil, err - } - - var htlcs []HTLC - if numHtlcs == 0 { - return htlcs, nil - } - - htlcs = make([]HTLC, numHtlcs) - for i := uint16(0); i < numHtlcs; i++ { - var onionAndExtraData []byte - if err := ReadElements(r, - &htlcs[i].Signature, &htlcs[i].RHash, &htlcs[i].Amt, - &htlcs[i].RefundTimeout, &htlcs[i].OutputIndex, - &htlcs[i].Incoming, &onionAndExtraData, - &htlcs[i].HtlcIndex, &htlcs[i].LogIndex, - ); err != nil { - return htlcs, err - } - - // Sanity check that we have at least the onion blob size we - // expect. - if len(onionAndExtraData) < lnwire.OnionPacketSize { - return nil, ErrOnionBlobLength - } - - // First OnionPacketSize bytes are our fixed length onion - // packet. - copy( - htlcs[i].OnionBlob[:], - onionAndExtraData[0:lnwire.OnionPacketSize], - ) - - // Any additional bytes belong to extra data. ExtraDataLen - // will be >= 0, because we know that we always have a fixed - // length onion packet. - extraDataLen := len(onionAndExtraData) - lnwire.OnionPacketSize - if extraDataLen > 0 { - htlcs[i].ExtraData = make([]byte, extraDataLen) - - copy( - htlcs[i].ExtraData, - onionAndExtraData[lnwire.OnionPacketSize:], - ) - } - - // Finally, deserialize any TLVs contained in that extra data - // if they are present. - if err := deserializeHtlcExtraData(&htlcs[i]); err != nil { - return nil, err - } - } - - return htlcs, nil + return cstate.DeserializeHtlcs(r) } // serializeLogUpdate writes a log update to the provided io.Writer. diff --git a/chanstate/htlc_serialization.go b/chanstate/htlc_serialization.go new file mode 100644 index 0000000000..9083c46490 --- /dev/null +++ b/chanstate/htlc_serialization.go @@ -0,0 +1,191 @@ +package chanstate + +import ( + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// serializeHtlcExtraData encodes a TLV stream of extra data to be stored with a +// HTLC. It uses the update_add_htlc TLV types, because this is where extra +// data is passed with a HTLC. At present blinding points are the only extra +// data that we will store, and the function is a no-op if a nil blinding +// point is provided. +// +// This function MUST be called to persist all HTLC values when they are +// serialized. +func serializeHtlcExtraData(h *HTLC) error { + var records []tlv.RecordProducer + h.BlindingPoint.WhenSome(func(b tlv.RecordT[lnwire.BlindingPointTlvType, + *btcec.PublicKey]) { + + records = append(records, &b) + }) + + records, err := h.CustomRecords.ExtendRecordProducers(records) + if err != nil { + return err + } + + return h.ExtraData.PackRecords(records...) +} + +// deserializeHtlcExtraData extracts TLVs from the extra data persisted for the +// HTLC and populates values in the struct accordingly. +// +// This function MUST be called to populate the struct properly when HTLCs +// are deserialized. +func deserializeHtlcExtraData(h *HTLC) error { + if len(h.ExtraData) == 0 { + return nil + } + + blindingPoint := h.BlindingPoint.Zero() + tlvMap, err := h.ExtraData.ExtractRecords(&blindingPoint) + if err != nil { + return err + } + + if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil { + h.BlindingPoint = tlv.SomeRecordT(blindingPoint) + + // Remove the entry from the TLV map. Anything left in the map + // will be included in the custom records field. + delete(tlvMap, h.BlindingPoint.TlvType()) + } + + // Set the custom records field to the remaining TLV records. + customRecords, err := lnwire.NewCustomRecords(tlvMap) + if err != nil { + return err + } + h.CustomRecords = customRecords + + return nil +} + +// SerializeHtlcs writes out the passed set of HTLC's into the passed writer +// using the current default on-disk serialization format. +// +// This inline serialization has been extended to allow storage of extra data +// associated with a HTLC in the following way: +// - The known-length onion blob (1366 bytes) is serialized as var bytes in +// WriteElements (ie, the length 1366 was written, followed by the 1366 +// onion bytes). +// - To include extra data, we append any extra data present to this one +// variable length of data. Since we know that the onion is strictly 1366 +// bytes, any length after that should be considered to be extra data. +// +// NOTE: This API is NOT stable, the on-disk format will likely change in the +// future. +func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error { + numHtlcs := uint16(len(htlcs)) + if err := WriteElement(b, numHtlcs); err != nil { + return err + } + + for _, htlc := range htlcs { + // Populate TLV stream for any additional fields contained + // in the TLV. + if err := serializeHtlcExtraData(&htlc); err != nil { + return err + } + + // The onion blob and hltc data are stored as a single var + // bytes blob. + onionAndExtraData := make( + []byte, lnwire.OnionPacketSize+len(htlc.ExtraData), + ) + copy(onionAndExtraData, htlc.OnionBlob[:]) + copy(onionAndExtraData[lnwire.OnionPacketSize:], htlc.ExtraData) + + if err := WriteElements(b, + //nolint:ll + htlc.Signature, htlc.RHash, htlc.Amt, htlc.RefundTimeout, + htlc.OutputIndex, htlc.Incoming, onionAndExtraData, + htlc.HtlcIndex, htlc.LogIndex, + ); err != nil { + return err + } + } + + return nil +} + +// DeserializeHtlcs attempts to read out a slice of HTLC's from the passed +// io.Reader. The bytes within the passed reader MUST have been previously +// written to using the SerializeHtlcs function. +// +// This inline deserialization has been extended to allow storage of extra data +// associated with a HTLC in the following way: +// - The known-length onion blob (1366 bytes) and any additional data present +// are read out as a single blob of variable byte data. +// - They are stored like this to take advantage of the variable space +// available for extension without migration (see SerializeHtlcs). +// - The first 1366 bytes are interpreted as the onion blob, and any remaining +// bytes as extra HTLC data. +// - This extra HTLC data is expected to be serialized as a TLV stream, and +// its parsing is left to higher layers. +// +// NOTE: This API is NOT stable, the on-disk format will likely change in the +// future. +func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { + var numHtlcs uint16 + if err := ReadElement(r, &numHtlcs); err != nil { + return nil, err + } + + var htlcs []HTLC + if numHtlcs == 0 { + return htlcs, nil + } + + htlcs = make([]HTLC, numHtlcs) + for i := uint16(0); i < numHtlcs; i++ { + var onionAndExtraData []byte + if err := ReadElements(r, + &htlcs[i].Signature, &htlcs[i].RHash, &htlcs[i].Amt, + &htlcs[i].RefundTimeout, &htlcs[i].OutputIndex, + &htlcs[i].Incoming, &onionAndExtraData, + &htlcs[i].HtlcIndex, &htlcs[i].LogIndex, + ); err != nil { + return htlcs, err + } + + // Sanity check that we have at least the onion blob size we + // expect. + if len(onionAndExtraData) < lnwire.OnionPacketSize { + return nil, ErrOnionBlobLength + } + + // First OnionPacketSize bytes are our fixed length onion + // packet. + copy( + htlcs[i].OnionBlob[:], + onionAndExtraData[0:lnwire.OnionPacketSize], + ) + + // Any additional bytes belong to extra data. ExtraDataLen + // will be >= 0, because we know that we always have a fixed + // length onion packet. + extraDataLen := len(onionAndExtraData) - lnwire.OnionPacketSize + if extraDataLen > 0 { + htlcs[i].ExtraData = make([]byte, extraDataLen) + + copy( + htlcs[i].ExtraData, + onionAndExtraData[lnwire.OnionPacketSize:], + ) + } + + // Finally, deserialize any TLVs contained in that extra data + // if they are present. + if err := deserializeHtlcExtraData(&htlcs[i]); err != nil { + return nil, err + } + } + + return htlcs, nil +} From ac163785694969d25ea356fef00a784a2670ecab Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:27:47 -0300 Subject: [PATCH 16/61] chanstate: move log update serialization Move the log update slice serialization helpers into chanstate and make channeldb call the chanstate helpers directly. This keeps forwarding package log serialization and open-channel log update serialization on the same channel-state codec path. --- channeldb/channel.go | 71 +++++------------------------------ chanstate/kv_serialization.go | 63 +++++++++++++++++-------------- 2 files changed, 44 insertions(+), 90 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 50d4ed6464..7639fc6129 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1314,7 +1314,7 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, // Persist unsigned but acked remote updates that need to be // restored after a restart. var b bytes.Buffer - err = serializeLogUpdates(&b, unsignedAckedUpdates) + err = cstate.SerializeLogUpdates(&b, unsignedAckedUpdates) if err != nil { return err } @@ -1344,7 +1344,7 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, } r := bytes.NewReader(updateBytes) - updates, err := deserializeLogUpdates(r) + updates, err := cstate.DeserializeLogUpdates(r) if err != nil { return err } @@ -1383,7 +1383,7 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, } var b3 bytes.Buffer - err = serializeLogUpdates(&b3, unsignedUpdates) + err = cstate.SerializeLogUpdates(&b3, unsignedUpdates) if err != nil { return fmt.Errorf("unable to serialize log updates: %w", err) @@ -1453,57 +1453,6 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { return cstate.DeserializeHtlcs(r) } -// serializeLogUpdate writes a log update to the provided io.Writer. -func serializeLogUpdate(w io.Writer, l *LogUpdate) error { - return WriteElements(w, l.LogIndex, l.UpdateMsg) -} - -// deserializeLogUpdate reads a log update from the provided io.Reader. -func deserializeLogUpdate(r io.Reader) (*LogUpdate, error) { - l := &LogUpdate{} - if err := ReadElements(r, &l.LogIndex, &l.UpdateMsg); err != nil { - return nil, err - } - - return l, nil -} - -// serializeLogUpdates serializes provided list of updates to a stream. -func serializeLogUpdates(w io.Writer, logUpdates []LogUpdate) error { - numUpdates := uint16(len(logUpdates)) - if err := binary.Write(w, byteOrder, numUpdates); err != nil { - return err - } - - for _, diff := range logUpdates { - err := WriteElements(w, diff.LogIndex, diff.UpdateMsg) - if err != nil { - return err - } - } - - return nil -} - -// deserializeLogUpdates deserializes a list of updates from a stream. -func deserializeLogUpdates(r io.Reader) ([]LogUpdate, error) { - var numUpdates uint16 - if err := binary.Read(r, byteOrder, &numUpdates); err != nil { - return nil, err - } - - logUpdates := make([]LogUpdate, numUpdates) - for i := 0; i < int(numUpdates); i++ { - err := ReadElements(r, - &logUpdates[i].LogIndex, &logUpdates[i].UpdateMsg, - ) - if err != nil { - return nil, err - } - } - return logUpdates, nil -} - func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl if err := serializeChanCommit(w, &diff.Commitment); err != nil { return err @@ -1513,7 +1462,7 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl return err } - if err := serializeLogUpdates(w, diff.LogUpdates); err != nil { + if err := cstate.SerializeLogUpdates(w, diff.LogUpdates); err != nil { return err } @@ -1574,7 +1523,7 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) { } d.CommitSig = commitSig - d.LogUpdates, err = deserializeLogUpdates(r) + d.LogUpdates, err = cstate.DeserializeLogUpdates(r) if err != nil { return nil, err } @@ -1762,7 +1711,7 @@ func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( } r := bytes.NewReader(updateBytes) - updates, err = deserializeLogUpdates(r) + updates, err = cstate.DeserializeLogUpdates(r) return err }, func() { updates = nil @@ -1800,7 +1749,7 @@ func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( } r := bytes.NewReader(updateBytes) - updates, err = deserializeLogUpdates(r) + updates, err = cstate.DeserializeLogUpdates(r) return err }, func() { updates = nil @@ -1936,7 +1885,7 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, } r := bytes.NewReader(updateBytes) - unsignedUpdates, err := deserializeLogUpdates(r) + unsignedUpdates, err := cstate.DeserializeLogUpdates(r) if err != nil { return err } @@ -1953,7 +1902,7 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, } var b bytes.Buffer - err = serializeLogUpdates(&b, validUpdates) + err = cstate.SerializeLogUpdates(&b, validUpdates) if err != nil { return fmt.Errorf("unable to serialize log updates: %w", err) @@ -1968,7 +1917,7 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, // Persist the local updates the peer hasn't yet signed so they // can be restored after restart. var b2 bytes.Buffer - err = serializeLogUpdates(&b2, updates) + err = cstate.SerializeLogUpdates(&b2, updates) if err != nil { return err } diff --git a/chanstate/kv_serialization.go b/chanstate/kv_serialization.go index f40e13daaa..464e18d227 100644 --- a/chanstate/kv_serialization.go +++ b/chanstate/kv_serialization.go @@ -1,57 +1,62 @@ package chanstate import ( - "bytes" "encoding/binary" "io" - - "github.com/lightningnetwork/lnd/lnwire" ) var byteOrder = binary.BigEndian // serializeLogUpdate writes a log update to the provided io.Writer. func serializeLogUpdate(w io.Writer, l *LogUpdate) error { - if err := binary.Write(w, byteOrder, l.LogIndex); err != nil { - return err - } - - var msgBuf bytes.Buffer - if _, err := lnwire.WriteMessage(&msgBuf, l.UpdateMsg, 0); err != nil { - return err - } - - msgLen := uint16(msgBuf.Len()) - if err := binary.Write(w, byteOrder, msgLen); err != nil { - return err - } - - _, err := w.Write(msgBuf.Bytes()) - - return err + return WriteElements(w, l.LogIndex, l.UpdateMsg) } // deserializeLogUpdate reads a log update from the provided io.Reader. func deserializeLogUpdate(r io.Reader) (*LogUpdate, error) { l := &LogUpdate{} - if err := binary.Read(r, byteOrder, &l.LogIndex); err != nil { + if err := ReadElements(r, &l.LogIndex, &l.UpdateMsg); err != nil { return nil, err } - var msgLen uint16 - if err := binary.Read(r, byteOrder, &msgLen); err != nil { - return nil, err + return l, nil +} + +// SerializeLogUpdates serializes provided list of updates to a stream. +func SerializeLogUpdates(w io.Writer, logUpdates []LogUpdate) error { + numUpdates := uint16(len(logUpdates)) + if err := binary.Write(w, byteOrder, numUpdates); err != nil { + return err } - msgReader := io.LimitReader(r, int64(msgLen)) - msg, err := lnwire.ReadMessage(msgReader, 0) - if err != nil { + for _, diff := range logUpdates { + err := WriteElements(w, diff.LogIndex, diff.UpdateMsg) + if err != nil { + return err + } + } + + return nil +} + +// DeserializeLogUpdates deserializes a list of updates from a stream. +func DeserializeLogUpdates(r io.Reader) ([]LogUpdate, error) { + var numUpdates uint16 + if err := binary.Read(r, byteOrder, &numUpdates); err != nil { return nil, err } - l.UpdateMsg = msg + logUpdates := make([]LogUpdate, numUpdates) + for i := 0; i < int(numUpdates); i++ { + err := ReadElements(r, + &logUpdates[i].LogIndex, &logUpdates[i].UpdateMsg, + ) + if err != nil { + return nil, err + } + } - return l, nil + return logUpdates, nil } // makeLogKey converts a uint64 into an 8 byte array. From ab09a0332fae61911969e4d30a72ecb4b05f7ef2 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:31:12 -0300 Subject: [PATCH 17/61] chanstate: move commitment serialization Move channel commitment serialization into chanstate now that the codec and HTLC serialization helpers are available there. Leave thin channeldb wrappers for legacy revocation-log callers while the remaining channeldb channel state code is split apart. --- channeldb/channel.go | 37 +++++-------------------- chanstate/commitment_serialization.go | 39 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 31 deletions(-) create mode 100644 chanstate/commitment_serialization.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 7639fc6129..c9fa6c4932 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1454,7 +1454,7 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { } func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl - if err := serializeChanCommit(w, &diff.Commitment); err != nil { + if err := cstate.SerializeChanCommit(w, &diff.Commitment); err != nil { return err } @@ -1507,7 +1507,7 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) { err error ) - d.Commitment, err = deserializeChanCommit(r) + d.Commitment, err = cstate.DeserializeChanCommit(r) if err != nil { return nil, err } @@ -2704,16 +2704,7 @@ func getOptionalUpfrontShutdownScript(chanBucket kvdb.RBucket, key []byte, } func serializeChanCommit(w io.Writer, c *ChannelCommitment) error { - if err := WriteElements(w, - c.CommitHeight, c.LocalLogIndex, c.LocalHtlcIndex, - c.RemoteLogIndex, c.RemoteHtlcIndex, c.LocalBalance, - c.RemoteBalance, c.CommitFee, c.FeePerKw, c.CommitTx, - c.CommitSig, - ); err != nil { - return err - } - - return SerializeHtlcs(w, c.Htlcs...) + return cstate.SerializeChanCommit(w, c) } func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, @@ -2727,7 +2718,7 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, } var b bytes.Buffer - if err := serializeChanCommit(&b, c); err != nil { + if err := cstate.SerializeChanCommit(&b, c); err != nil { return err } @@ -2863,23 +2854,7 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { } func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) { - var c ChannelCommitment - - err := ReadElements(r, - &c.CommitHeight, &c.LocalLogIndex, &c.LocalHtlcIndex, &c.RemoteLogIndex, - &c.RemoteHtlcIndex, &c.LocalBalance, &c.RemoteBalance, - &c.CommitFee, &c.FeePerKw, &c.CommitTx, &c.CommitSig, - ) - if err != nil { - return c, err - } - - c.Htlcs, err = DeserializeHtlcs(r) - if err != nil { - return c, err - } - - return c, nil + return cstate.DeserializeChanCommit(r) } func fetchChanCommitment(chanBucket kvdb.RBucket, @@ -2898,7 +2873,7 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, } r := bytes.NewReader(commitBytes) - chanCommit, err := deserializeChanCommit(r) + chanCommit, err := cstate.DeserializeChanCommit(r) if err != nil { return ChannelCommitment{}, fmt.Errorf("unable to decode "+ "chan commit: %w", err) diff --git a/chanstate/commitment_serialization.go b/chanstate/commitment_serialization.go new file mode 100644 index 0000000000..fc294b2ea5 --- /dev/null +++ b/chanstate/commitment_serialization.go @@ -0,0 +1,39 @@ +package chanstate + +import "io" + +// SerializeChanCommit serializes the channel commitment. +func SerializeChanCommit(w io.Writer, c *ChannelCommitment) error { + if err := WriteElements(w, + c.CommitHeight, c.LocalLogIndex, c.LocalHtlcIndex, + c.RemoteLogIndex, c.RemoteHtlcIndex, c.LocalBalance, + c.RemoteBalance, c.CommitFee, c.FeePerKw, c.CommitTx, + c.CommitSig, + ); err != nil { + return err + } + + return SerializeHtlcs(w, c.Htlcs...) +} + +// DeserializeChanCommit deserializes the channel commitment. +func DeserializeChanCommit(r io.Reader) (ChannelCommitment, error) { + var c ChannelCommitment + + err := ReadElements(r, + &c.CommitHeight, &c.LocalLogIndex, &c.LocalHtlcIndex, + &c.RemoteLogIndex, &c.RemoteHtlcIndex, &c.LocalBalance, + &c.RemoteBalance, &c.CommitFee, &c.FeePerKw, &c.CommitTx, + &c.CommitSig, + ) + if err != nil { + return c, err + } + + c.Htlcs, err = DeserializeHtlcs(r) + if err != nil { + return c, err + } + + return c, nil +} From 87c7ba36a304ad8f7bf84058efb7033479ba75c9 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:36:28 -0300 Subject: [PATCH 18/61] chanstate: move commit diff serialization Move commit diff serialization and the commitment auxiliary TLV helpers into chanstate so pending remote commitment state no longer depends on channeldb codecs. Update channeldb callers to use the chanstate helpers while leaving open-channel bucket persistence in place for later backend-specific splits. --- channeldb/channel.go | 202 +------------------------ chanstate/commit_diff_serialization.go | 127 ++++++++++++++++ chanstate/commit_tlv_data.go | 94 ++++++++++++ 3 files changed, 228 insertions(+), 195 deletions(-) create mode 100644 chanstate/commit_diff_serialization.go create mode 100644 chanstate/commit_tlv_data.go diff --git a/channeldb/channel.go b/channeldb/channel.go index c9fa6c4932..bc094d7e97 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2,7 +2,6 @@ package channeldb import ( "bytes" - "encoding/binary" "errors" "fmt" "io" @@ -14,7 +13,6 @@ import ( cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" - "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lntypes" @@ -342,73 +340,6 @@ type CommitmentParams = cstate.CommitmentParams // ChannelConfig houses the channel configuration for one side of a channel. type ChannelConfig = cstate.ChannelConfig -// commitTlvData stores all the optional data that may be stored as a TLV stream -// at the _end_ of the normal serialized commit on disk. -type commitTlvData struct { - // customBlob is a custom blob that may store extra data for custom - // channels. - customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob] -} - -// encode encodes the aux data into the passed io.Writer. -func (c *commitTlvData) encode(w io.Writer) error { - var tlvRecords []tlv.Record - c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) { - tlvRecords = append(tlvRecords, blob.Record()) - }) - - // Create the tlv stream. - tlvStream, err := tlv.NewStream(tlvRecords...) - if err != nil { - return err - } - - return tlvStream.Encode(w) -} - -// decode attempts to decode the aux data from the passed io.Reader. -func (c *commitTlvData) decode(r io.Reader) error { - blob := c.customBlob.Zero() - - tlvStream, err := tlv.NewStream( - blob.Record(), - ) - if err != nil { - return err - } - - tlvs, err := tlvStream.DecodeWithParsedTypes(r) - if err != nil { - return err - } - - if _, ok := tlvs[c.customBlob.TlvType()]; ok { - c.customBlob = tlv.SomeRecordT(blob) - } - - return nil -} - -// amendCommitTlvData updates the commitment with the given auxiliary TLV data. -func amendCommitTlvData(c *ChannelCommitment, auxData commitTlvData) { - auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { - c.CustomBlob = fn.Some(blob) - }) -} - -// extractCommitTlvData creates a new commitTlvData from the given commitment. -func extractCommitTlvData(c *ChannelCommitment) commitTlvData { - var auxData commitTlvData - - c.CustomBlob.WhenSome(func(blob tlv.Blob) { - auxData.customBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType1](blob), - ) - }) - - return auxData -} - // ChannelStatus is a bit vector used to indicate whether an OpenChannel is in // the default usable state, or a state where it shouldn't be used. type ChannelStatus = cstate.ChannelStatus @@ -1453,123 +1384,8 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { return cstate.DeserializeHtlcs(r) } -func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl - if err := cstate.SerializeChanCommit(w, &diff.Commitment); err != nil { - return err - } - - if err := WriteElements(w, diff.CommitSig); err != nil { - return err - } - - if err := cstate.SerializeLogUpdates(w, diff.LogUpdates); err != nil { - return err - } - - numOpenRefs := uint16(len(diff.OpenedCircuitKeys)) - if err := binary.Write(w, byteOrder, numOpenRefs); err != nil { - return err - } - - for _, openRef := range diff.OpenedCircuitKeys { - err := WriteElements(w, openRef.ChanID, openRef.HtlcID) - if err != nil { - return err - } - } - - numClosedRefs := uint16(len(diff.ClosedCircuitKeys)) - if err := binary.Write(w, byteOrder, numClosedRefs); err != nil { - return err - } - - for _, closedRef := range diff.ClosedCircuitKeys { - err := WriteElements(w, closedRef.ChanID, closedRef.HtlcID) - if err != nil { - return err - } - } - - // We'll also encode the commit aux data stream here. We do this here - // rather than above (at the call to serializeChanCommit), to ensure - // backwards compat for reads to existing non-custom channels. - auxData := extractCommitTlvData(&diff.Commitment) - if err := auxData.encode(w); err != nil { - return fmt.Errorf("unable to write aux data: %w", err) - } - - return nil -} - -func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) { - var ( - d CommitDiff - err error - ) - - d.Commitment, err = cstate.DeserializeChanCommit(r) - if err != nil { - return nil, err - } - - var msg lnwire.Message - if err := ReadElements(r, &msg); err != nil { - return nil, err - } - commitSig, ok := msg.(*lnwire.CommitSig) - if !ok { - return nil, fmt.Errorf("expected lnwire.CommitSig, instead "+ - "read: %T", msg) - } - d.CommitSig = commitSig - - d.LogUpdates, err = cstate.DeserializeLogUpdates(r) - if err != nil { - return nil, err - } - - var numOpenRefs uint16 - if err := binary.Read(r, byteOrder, &numOpenRefs); err != nil { - return nil, err - } - - d.OpenedCircuitKeys = make([]models.CircuitKey, numOpenRefs) - for i := 0; i < int(numOpenRefs); i++ { - err := ReadElements(r, - &d.OpenedCircuitKeys[i].ChanID, - &d.OpenedCircuitKeys[i].HtlcID) - if err != nil { - return nil, err - } - } - - var numClosedRefs uint16 - if err := binary.Read(r, byteOrder, &numClosedRefs); err != nil { - return nil, err - } - - d.ClosedCircuitKeys = make([]models.CircuitKey, numClosedRefs) - for i := 0; i < int(numClosedRefs); i++ { - err := ReadElements(r, - &d.ClosedCircuitKeys[i].ChanID, - &d.ClosedCircuitKeys[i].HtlcID) - if err != nil { - return nil, err - } - } - - // As a final step, we'll read out any aux commit data that we have at - // the end of this byte stream. We do this here to ensure backward - // compatibility, as otherwise we risk erroneously reading into the - // wrong field. - var auxData commitTlvData - if err := auxData.decode(r); err != nil { - return nil, fmt.Errorf("unable to decode aux data: %w", err) - } - - amendCommitTlvData(&d.Commitment, auxData) - - return &d, nil +func newChannelPackager(channel *OpenChannel) *ChannelPackager { + return NewChannelPackager(channel.ShortChannelID) } // AppendRemoteCommitChain appends a new CommitDiff to the remote party's @@ -1637,7 +1453,7 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, // With the bucket retrieved, we'll now serialize the commit // diff itself, and write it to disk. var b2 bytes.Buffer - if err := serializeCommitDiff(&b2, diff); err != nil { + if err := cstate.SerializeCommitDiff(&b2, diff); err != nil { return err } return chanBucket.Put(commitDiffKey, b2.Bytes()) @@ -1669,7 +1485,7 @@ func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( } tipReader := bytes.NewReader(tipBytes) - dcd, err := deserializeCommitDiff(tipReader) + dcd, err := cstate.DeserializeCommitDiff(tipReader) if err != nil { return err } @@ -1840,7 +1656,7 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, // with the current locked-in commitment for the remote party. tipBytes := chanBucket.Get(commitDiffKey) tipReader := bytes.NewReader(tipBytes) - newCommit, err := deserializeCommitDiff(tipReader) + newCommit, err := cstate.DeserializeCommitDiff(tipReader) if err != nil { return err } @@ -2723,8 +2539,7 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, } // Before we write to disk, we'll also write our aux data as well. - auxData := extractCommitTlvData(c) - if err := auxData.encode(&b); err != nil { + if err := cstate.EncodeCommitTlvData(&b, c); err != nil { return fmt.Errorf("unable to write aux data: %w", err) } @@ -2881,14 +2696,11 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, // We'll also check to see if we have any aux data stored as the end of // the stream. - var auxData commitTlvData - if err := auxData.decode(r); err != nil { + if err := cstate.DecodeCommitTlvData(r, &chanCommit); err != nil { return ChannelCommitment{}, fmt.Errorf("unable to decode "+ "chan aux data: %w", err) } - amendCommitTlvData(&chanCommit, auxData) - return chanCommit, nil } diff --git a/chanstate/commit_diff_serialization.go b/chanstate/commit_diff_serialization.go new file mode 100644 index 0000000000..f34a9f6804 --- /dev/null +++ b/chanstate/commit_diff_serialization.go @@ -0,0 +1,127 @@ +package chanstate + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/lnwire" +) + +// SerializeCommitDiff serializes the commit diff. +func SerializeCommitDiff(w io.Writer, diff *CommitDiff) error { + if err := SerializeChanCommit(w, &diff.Commitment); err != nil { + return err + } + + if err := WriteElements(w, diff.CommitSig); err != nil { + return err + } + + if err := SerializeLogUpdates(w, diff.LogUpdates); err != nil { + return err + } + + numOpenRefs := uint16(len(diff.OpenedCircuitKeys)) + if err := binary.Write(w, byteOrder, numOpenRefs); err != nil { + return err + } + + for _, openRef := range diff.OpenedCircuitKeys { + err := WriteElements(w, openRef.ChanID, openRef.HtlcID) + if err != nil { + return err + } + } + + numClosedRefs := uint16(len(diff.ClosedCircuitKeys)) + if err := binary.Write(w, byteOrder, numClosedRefs); err != nil { + return err + } + + for _, closedRef := range diff.ClosedCircuitKeys { + err := WriteElements(w, closedRef.ChanID, closedRef.HtlcID) + if err != nil { + return err + } + } + + // We'll also encode the commit aux data stream here. We do this here + // rather than above (at the call to serializeChanCommit), to ensure + // backwards compat for reads to existing non-custom channels. + if err := EncodeCommitTlvData(w, &diff.Commitment); err != nil { + return fmt.Errorf("unable to write aux data: %w", err) + } + + return nil +} + +// DeserializeCommitDiff deserializes the commit diff. +func DeserializeCommitDiff(r io.Reader) (*CommitDiff, error) { + var ( + d CommitDiff + err error + ) + + d.Commitment, err = DeserializeChanCommit(r) + if err != nil { + return nil, err + } + + var msg lnwire.Message + if err := ReadElements(r, &msg); err != nil { + return nil, err + } + commitSig, ok := msg.(*lnwire.CommitSig) + if !ok { + return nil, fmt.Errorf("expected lnwire.CommitSig, instead "+ + "read: %T", msg) + } + d.CommitSig = commitSig + + d.LogUpdates, err = DeserializeLogUpdates(r) + if err != nil { + return nil, err + } + + var numOpenRefs uint16 + if err := binary.Read(r, byteOrder, &numOpenRefs); err != nil { + return nil, err + } + + d.OpenedCircuitKeys = make([]models.CircuitKey, numOpenRefs) + for i := 0; i < int(numOpenRefs); i++ { + err := ReadElements(r, + &d.OpenedCircuitKeys[i].ChanID, + &d.OpenedCircuitKeys[i].HtlcID) + if err != nil { + return nil, err + } + } + + var numClosedRefs uint16 + if err := binary.Read(r, byteOrder, &numClosedRefs); err != nil { + return nil, err + } + + d.ClosedCircuitKeys = make([]models.CircuitKey, numClosedRefs) + for i := 0; i < int(numClosedRefs); i++ { + err := ReadElements(r, + &d.ClosedCircuitKeys[i].ChanID, + &d.ClosedCircuitKeys[i].HtlcID) + if err != nil { + return nil, err + } + } + + // As a final step, we'll read out any aux commit data that we have at + // the end of this byte stream. We do this here to ensure backward + // compatibility, as otherwise we risk erroneously reading into the + // wrong field. + if err := DecodeCommitTlvData(r, &d.Commitment); err != nil { + return nil, fmt.Errorf("unable to decode aux data: %w", err) + } + + return &d, nil +} diff --git a/chanstate/commit_tlv_data.go b/chanstate/commit_tlv_data.go new file mode 100644 index 0000000000..e3bee99f54 --- /dev/null +++ b/chanstate/commit_tlv_data.go @@ -0,0 +1,94 @@ +package chanstate + +import ( + "io" + + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/tlv" +) + +// commitTlvData stores all the optional data that may be stored as a TLV stream +// at the _end_ of the normal serialized commit on disk. +type commitTlvData struct { + // customBlob is a custom blob that may store extra data for custom + // channels. + customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob] +} + +// encode encodes the aux data into the passed io.Writer. +func (c *commitTlvData) encode(w io.Writer) error { + var tlvRecords []tlv.Record + c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) { + tlvRecords = append(tlvRecords, blob.Record()) + }) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// decode attempts to decode the aux data from the passed io.Reader. +func (c *commitTlvData) decode(r io.Reader) error { + blob := c.customBlob.Zero() + + tlvStream, err := tlv.NewStream( + blob.Record(), + ) + if err != nil { + return err + } + + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[c.customBlob.TlvType()]; ok { + c.customBlob = tlv.SomeRecordT(blob) + } + + return nil +} + +// DecodeCommitTlvData decodes and applies auxiliary TLV data to a commitment. +func DecodeCommitTlvData(r io.Reader, c *ChannelCommitment) error { + var auxData commitTlvData + if err := auxData.decode(r); err != nil { + return err + } + + amendCommitTlvData(c, auxData) + + return nil +} + +// EncodeCommitTlvData extracts and encodes auxiliary TLV data from a +// commitment. +func EncodeCommitTlvData(w io.Writer, c *ChannelCommitment) error { + auxData := extractCommitTlvData(c) + return auxData.encode(w) +} + +// amendCommitTlvData updates the commitment with the given auxiliary TLV data. +func amendCommitTlvData(c *ChannelCommitment, auxData commitTlvData) { + auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { + c.CustomBlob = fn.Some(blob) + }) +} + +// extractCommitTlvData creates a new commitTlvData from the given commitment. +func extractCommitTlvData(c *ChannelCommitment) commitTlvData { + var auxData commitTlvData + + c.CustomBlob.WhenSome(func(blob tlv.Blob) { + auxData.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType1](blob), + ) + }) + + return auxData +} From b18328e43016cc177f0381495bebc20630751873 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:44:20 -0300 Subject: [PATCH 19/61] chanstate: move open channel tlv data Move open-channel auxiliary TLV serialization and the key-locator TLV adapter into chanstate with the rest of the channel-state codecs. Keep channeldb compatibility wrappers for the exported key-locator encoder helpers used by existing tests. --- channeldb/channel.go | 250 +---------------------------- chanstate/key_locator.go | 57 +++++++ chanstate/open_channel_tlv_data.go | 228 ++++++++++++++++++++++++++ 3 files changed, 289 insertions(+), 246 deletions(-) create mode 100644 chanstate/key_locator.go create mode 100644 chanstate/open_channel_tlv_data.go diff --git a/channeldb/channel.go b/channeldb/channel.go index bc094d7e97..89c89565b8 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -13,7 +13,6 @@ import ( cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" - "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -128,132 +127,6 @@ const ( outpointClosed = cstate.OutpointClosed ) -// openChannelTlvData houses the new data fields that are stored for each -// channel in a TLV stream within the root bucket. This is stored as a TLV -// stream appended to the existing hard-coded fields in the channel's root -// bucket. New fields being added to the channel state should be added here. -// -// NOTE: This struct is used for serialization purposes only and its fields -// should be accessed via the OpenChannel struct while in memory. -type openChannelTlvData struct { - // revokeKeyLoc is the key locator for the revocation key. - revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord] - - // initialLocalBalance is the initial local balance of the channel. - initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64] - - // initialRemoteBalance is the initial remote balance of the channel. - initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64] - - // realScid is the real short channel ID of the channel corresponding to - // the on-chain outpoint. - realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID] - - // memo is an optional text field that gives context to the user about - // the channel. - memo tlv.OptionalRecordT[tlv.TlvType5, []byte] - - // tapscriptRoot is the optional Tapscript root the channel funding - // output commits to. - tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] - - // customBlob is an optional TLV encoded blob of data representing - // custom channel funding information. - customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob] - - // confirmationHeight records the block height at which the funding - // transaction was first confirmed. - confirmationHeight tlv.RecordT[tlv.TlvType8, uint32] - - // closeConfirmationHeight records the block height at which the closing - // transaction was first confirmed. This is used to calculate the - // remaining confirmations until the channel is considered fully closed. - // Note: if not set, it means either the channel has not been - // closed yet, or it was closed before this field was introduced. - closeConfirmationHeight tlv.OptionalRecordT[tlv.TlvType9, uint32] -} - -// encode serializes the openChannelTlvData to the given io.Writer. -func (c *openChannelTlvData) encode(w io.Writer) error { - tlvRecords := []tlv.Record{ - c.revokeKeyLoc.Record(), - c.initialLocalBalance.Record(), - c.initialRemoteBalance.Record(), - c.realScid.Record(), - c.confirmationHeight.Record(), - } - c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { - tlvRecords = append(tlvRecords, memo.Record()) - }) - c.tapscriptRoot.WhenSome( - func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { - tlvRecords = append(tlvRecords, root.Record()) - }, - ) - c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) { - tlvRecords = append(tlvRecords, blob.Record()) - }) - c.closeConfirmationHeight.WhenSome( - func(h tlv.RecordT[tlv.TlvType9, uint32]) { - tlvRecords = append(tlvRecords, h.Record()) - }, - ) - - tlv.SortRecords(tlvRecords) - - // Create the tlv stream. - tlvStream, err := tlv.NewStream(tlvRecords...) - if err != nil { - return err - } - - return tlvStream.Encode(w) -} - -// decode deserializes the openChannelTlvData from the given io.Reader. -func (c *openChannelTlvData) decode(r io.Reader) error { - memo := c.memo.Zero() - tapscriptRoot := c.tapscriptRoot.Zero() - blob := c.customBlob.Zero() - closeConfHeight := c.closeConfirmationHeight.Zero() - - // Create the tlv stream. - tlvStream, err := tlv.NewStream( - c.revokeKeyLoc.Record(), - c.initialLocalBalance.Record(), - c.initialRemoteBalance.Record(), - c.realScid.Record(), - memo.Record(), - tapscriptRoot.Record(), - blob.Record(), - c.confirmationHeight.Record(), - closeConfHeight.Record(), - ) - if err != nil { - return err - } - - tlvs, err := tlvStream.DecodeWithParsedTypes(r) - if err != nil { - return err - } - - if _, ok := tlvs[memo.TlvType()]; ok { - c.memo = tlv.SomeRecordT(memo) - } - if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { - c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) - } - if _, ok := tlvs[c.customBlob.TlvType()]; ok { - c.customBlob = tlv.SomeRecordT(blob) - } - if _, ok := tlvs[closeConfHeight.TlvType()]; ok { - c.closeConfirmationHeight = tlv.SomeRecordT(closeConfHeight) - } - - return nil -} - // 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 @@ -391,78 +264,6 @@ const ( FinalHtlcOffchainBit = cstate.FinalHtlcOffchainBit ) -// amendOpenChannelTlvData updates the channel with the given auxiliary TLV -// data. -func amendOpenChannelTlvData(channel *OpenChannel, auxData openChannelTlvData) { - channel.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator - channel.InitialLocalBalance = lnwire.MilliSatoshi( - auxData.initialLocalBalance.Val, - ) - channel.InitialRemoteBalance = lnwire.MilliSatoshi( - auxData.initialRemoteBalance.Val, - ) - channel.SetConfirmedScidForStore(auxData.realScid.Val) - channel.ConfirmationHeight = auxData.confirmationHeight.Val - - auxData.memo.WhenSomeV(func(memo []byte) { - channel.Memo = memo - }) - auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) { - channel.TapscriptRoot = fn.Some[chainhash.Hash](h) - }) - auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { - channel.CustomBlob = fn.Some(blob) - }) - auxData.closeConfirmationHeight.WhenSomeV(func(h uint32) { - channel.CloseConfirmationHeight = fn.Some(h) - }) -} - -// extractOpenChannelTlvData creates a new openChannelTlvData from the given -// channel. -func extractOpenChannelTlvData(channel *OpenChannel) openChannelTlvData { - auxData := openChannelTlvData{ - revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1]( - keyLocRecord{channel.RevocationKeyLocator}, - ), - initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2]( - uint64(channel.InitialLocalBalance), - ), - initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3]( - uint64(channel.InitialRemoteBalance), - ), - realScid: tlv.NewRecordT[tlv.TlvType4]( - channel.ConfirmedScidForStore(), - ), - confirmationHeight: tlv.NewPrimitiveRecord[tlv.TlvType8]( - channel.ConfirmationHeight, - ), - } - - if len(channel.Memo) != 0 { - auxData.memo = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType5](channel.Memo), - ) - } - channel.TapscriptRoot.WhenSome(func(h chainhash.Hash) { - auxData.tapscriptRoot = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h), - ) - }) - channel.CustomBlob.WhenSome(func(blob tlv.Blob) { - auxData.customBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType7](blob), - ) - }) - channel.CloseConfirmationHeight.WhenSome(func(h uint32) { - auxData.closeConfirmationHeight = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType9](h), - ) - }) - - return auxData -} - // RefreshChannel updates the in-memory channel state using the latest state // observed on disk. func (c *ChannelStateDB) RefreshChannel(channel *OpenChannel) error { @@ -2458,8 +2259,7 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { return err } - auxData := extractOpenChannelTlvData(channel) - if err := auxData.encode(&w); err != nil { + if err := cstate.EncodeOpenChannelTlvData(&w, channel); err != nil { return fmt.Errorf("unable to encode aux data: %w", err) } @@ -2647,15 +2447,10 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { } } - var auxData openChannelTlvData - if err := auxData.decode(r); err != nil { + if err := cstate.DecodeOpenChannelTlvData(r, channel); err != nil { return fmt.Errorf("unable to decode aux data: %w", err) } - // Assign all the relevant fields from the aux data into the actual - // open channel. - amendOpenChannelTlvData(channel, auxData) - // Finally, read the optional shutdown scripts. if err := getOptionalUpfrontShutdownScript( chanBucket, localUpfrontShutdownKey, &channel.LocalShutdownScript, @@ -2795,51 +2590,14 @@ func deleteThawHeight(chanBucket kvdb.RwBucket) error { return cstate.DeleteThawHeight(chanBucket) } -// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the -// tlv.RecordProducer interface. -type keyLocRecord struct { - keychain.KeyLocator -} - -// Record creates a Record out of a KeyLocator using the passed Type and the -// EKeyLocator and DKeyLocator functions. The size will always be 8 as -// KeyFamily is uint32 and the Index is uint32. -// -// NOTE: This is part of the tlv.RecordProducer interface. -func (k *keyLocRecord) Record() tlv.Record { - // Note that we set the type here as zero, as when used with a - // tlv.RecordT, the type param will be used as the type. - return tlv.MakeStaticRecord( - 0, &k.KeyLocator, 8, EKeyLocator, DKeyLocator, - ) -} - // EKeyLocator is an encoder for keychain.KeyLocator. func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { - if v, ok := val.(*keychain.KeyLocator); ok { - err := tlv.EUint32T(w, uint32(v.Family), buf) - if err != nil { - return err - } - - return tlv.EUint32T(w, v.Index, buf) - } - return tlv.NewTypeForEncodingErr(val, "keychain.KeyLocator") + return cstate.EKeyLocator(w, val, buf) } // DKeyLocator is a decoder for keychain.KeyLocator. func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { - if v, ok := val.(*keychain.KeyLocator); ok { - var family uint32 - err := tlv.DUint32(r, &family, buf, 4) - if err != nil { - return err - } - v.Family = keychain.KeyFamily(family) - - return tlv.DUint32(r, &v.Index, buf, 4) - } - return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) + return cstate.DKeyLocator(r, val, buf, l) } // ShutdownInfo contains various info about the shutdown initiation of a diff --git a/chanstate/key_locator.go b/chanstate/key_locator.go new file mode 100644 index 0000000000..8ca357802d --- /dev/null +++ b/chanstate/key_locator.go @@ -0,0 +1,57 @@ +package chanstate + +import ( + "io" + + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/tlv" +) + +// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the +// tlv.RecordProducer interface. +type keyLocRecord struct { + keychain.KeyLocator +} + +// Record creates a Record out of a KeyLocator using the passed Type and the +// EKeyLocator and DKeyLocator functions. The size will always be 8 as +// KeyFamily is uint32 and the Index is uint32. +// +// NOTE: This is part of the tlv.RecordProducer interface. +func (k *keyLocRecord) Record() tlv.Record { + // Note that we set the type here as zero, as when used with a + // tlv.RecordT, the type param will be used as the type. + return tlv.MakeStaticRecord( + 0, &k.KeyLocator, 8, EKeyLocator, DKeyLocator, + ) +} + +// EKeyLocator is an encoder for keychain.KeyLocator. +func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*keychain.KeyLocator); ok { + err := tlv.EUint32T(w, uint32(v.Family), buf) + if err != nil { + return err + } + + return tlv.EUint32T(w, v.Index, buf) + } + + return tlv.NewTypeForEncodingErr(val, "keychain.KeyLocator") +} + +// DKeyLocator is a decoder for keychain.KeyLocator. +func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*keychain.KeyLocator); ok { + var family uint32 + err := tlv.DUint32(r, &family, buf, 4) + if err != nil { + return err + } + v.Family = keychain.KeyFamily(family) + + return tlv.DUint32(r, &v.Index, buf, 4) + } + + return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) +} diff --git a/chanstate/open_channel_tlv_data.go b/chanstate/open_channel_tlv_data.go new file mode 100644 index 0000000000..643f590332 --- /dev/null +++ b/chanstate/open_channel_tlv_data.go @@ -0,0 +1,228 @@ +package chanstate + +import ( + "io" + + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// openChannelTlvData houses the new data fields that are stored for each +// channel in a TLV stream within the root bucket. This is stored as a TLV +// stream appended to the existing hard-coded fields in the channel's root +// bucket. New fields being added to the channel state should be added here. +// +// NOTE: This struct is used for serialization purposes only and its fields +// should be accessed via the OpenChannel struct while in memory. +type openChannelTlvData struct { + // revokeKeyLoc is the key locator for the revocation key. + revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord] + + // initialLocalBalance is the initial local balance of the channel. + initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64] + + // initialRemoteBalance is the initial remote balance of the channel. + initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64] + + // realScid is the real short channel ID of the channel corresponding to + // the on-chain outpoint. + realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID] + + // memo is an optional text field that gives context to the user about + // the channel. + memo tlv.OptionalRecordT[tlv.TlvType5, []byte] + + // tapscriptRoot is the optional Tapscript root the channel funding + // output commits to. + tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] + + // customBlob is an optional TLV encoded blob of data representing + // custom channel funding information. + customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob] + + // confirmationHeight records the block height at which the funding + // transaction was first confirmed. + confirmationHeight tlv.RecordT[tlv.TlvType8, uint32] + + // closeConfirmationHeight records the block height at which the closing + // transaction was first confirmed. This is used to calculate the + // remaining confirmations until the channel is considered fully closed. + // Note: if not set, it means either the channel has not been + // closed yet, or it was closed before this field was introduced. + closeConfirmationHeight tlv.OptionalRecordT[tlv.TlvType9, uint32] +} + +// encode serializes the openChannelTlvData to the given io.Writer. +func (c *openChannelTlvData) encode(w io.Writer) error { + tlvRecords := []tlv.Record{ + c.revokeKeyLoc.Record(), + c.initialLocalBalance.Record(), + c.initialRemoteBalance.Record(), + c.realScid.Record(), + c.confirmationHeight.Record(), + } + c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { + tlvRecords = append(tlvRecords, memo.Record()) + }) + c.tapscriptRoot.WhenSome( + func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { + tlvRecords = append(tlvRecords, root.Record()) + }, + ) + c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) { + tlvRecords = append(tlvRecords, blob.Record()) + }) + c.closeConfirmationHeight.WhenSome( + func(h tlv.RecordT[tlv.TlvType9, uint32]) { + tlvRecords = append(tlvRecords, h.Record()) + }, + ) + + tlv.SortRecords(tlvRecords) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// decode deserializes the openChannelTlvData from the given io.Reader. +func (c *openChannelTlvData) decode(r io.Reader) error { + memo := c.memo.Zero() + tapscriptRoot := c.tapscriptRoot.Zero() + blob := c.customBlob.Zero() + closeConfHeight := c.closeConfirmationHeight.Zero() + + // Create the tlv stream. + tlvStream, err := tlv.NewStream( + c.revokeKeyLoc.Record(), + c.initialLocalBalance.Record(), + c.initialRemoteBalance.Record(), + c.realScid.Record(), + memo.Record(), + tapscriptRoot.Record(), + blob.Record(), + c.confirmationHeight.Record(), + closeConfHeight.Record(), + ) + if err != nil { + return err + } + + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[memo.TlvType()]; ok { + c.memo = tlv.SomeRecordT(memo) + } + if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { + c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) + } + if _, ok := tlvs[c.customBlob.TlvType()]; ok { + c.customBlob = tlv.SomeRecordT(blob) + } + if _, ok := tlvs[closeConfHeight.TlvType()]; ok { + c.closeConfirmationHeight = tlv.SomeRecordT(closeConfHeight) + } + + return nil +} + +// DecodeOpenChannelTlvData decodes and applies auxiliary TLV data to an open +// channel. +func DecodeOpenChannelTlvData(r io.Reader, channel *OpenChannel) error { + var auxData openChannelTlvData + if err := auxData.decode(r); err != nil { + return err + } + + amendOpenChannelTlvData(channel, auxData) + + return nil +} + +// EncodeOpenChannelTlvData extracts and encodes auxiliary TLV data from an open +// channel. +func EncodeOpenChannelTlvData(w io.Writer, channel *OpenChannel) error { + auxData := extractOpenChannelTlvData(channel) + return auxData.encode(w) +} + +// amendOpenChannelTlvData updates the channel with the given auxiliary TLV +// data. +func amendOpenChannelTlvData(channel *OpenChannel, auxData openChannelTlvData) { + channel.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator + channel.InitialLocalBalance = lnwire.MilliSatoshi( + auxData.initialLocalBalance.Val, + ) + channel.InitialRemoteBalance = lnwire.MilliSatoshi( + auxData.initialRemoteBalance.Val, + ) + channel.SetConfirmedScidForStore(auxData.realScid.Val) + channel.ConfirmationHeight = auxData.confirmationHeight.Val + + auxData.memo.WhenSomeV(func(memo []byte) { + channel.Memo = memo + }) + auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) { + channel.TapscriptRoot = fn.Some[chainhash.Hash](h) + }) + auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { + channel.CustomBlob = fn.Some(blob) + }) + auxData.closeConfirmationHeight.WhenSomeV(func(h uint32) { + channel.CloseConfirmationHeight = fn.Some(h) + }) +} + +// extractOpenChannelTlvData creates a new openChannelTlvData from the given +// channel. +func extractOpenChannelTlvData(channel *OpenChannel) openChannelTlvData { + auxData := openChannelTlvData{ + revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1]( + keyLocRecord{channel.RevocationKeyLocator}, + ), + initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2]( + uint64(channel.InitialLocalBalance), + ), + initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3]( + uint64(channel.InitialRemoteBalance), + ), + realScid: tlv.NewRecordT[tlv.TlvType4]( + channel.ConfirmedScidForStore(), + ), + confirmationHeight: tlv.NewPrimitiveRecord[tlv.TlvType8]( + channel.ConfirmationHeight, + ), + } + + if len(channel.Memo) != 0 { + auxData.memo = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5](channel.Memo), + ) + } + channel.TapscriptRoot.WhenSome(func(h chainhash.Hash) { + auxData.tapscriptRoot = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h), + ) + }) + channel.CustomBlob.WhenSome(func(blob tlv.Blob) { + auxData.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType7](blob), + ) + }) + channel.CloseConfirmationHeight.WhenSome(func(h uint32) { + auxData.closeConfirmationHeight = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType9](h), + ) + }) + + return auxData +} From a7bd57ef774c385471d7b984e1e2ceb88e5c0fcf Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:48:58 -0300 Subject: [PATCH 20/61] chanstate: move channel config serialization Move ChannelConfig serialization helpers next to the ChannelConfig type instead of adding a standalone serialization file. Keep channeldb wrappers for existing callers while later commits continue moving the surrounding open-channel storage code. --- channeldb/channel.go | 15 ++------------- chanstate/config.go | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 89c89565b8..dd9f9fd202 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2223,12 +2223,7 @@ func deserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) { } func writeChanConfig(b io.Writer, c *ChannelConfig) error { - return WriteElements(b, - c.DustLimit, c.MaxPendingAmount, c.ChanReserve, c.MinHTLC, - c.MaxAcceptedHtlcs, c.CsvDelay, c.MultiSigKey, - c.RevocationBasePoint, c.PaymentBasePoint, c.DelayBasePoint, - c.HtlcBasePoint, - ) + return cstate.WriteChanConfig(b, c) } func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { @@ -2388,13 +2383,7 @@ func putChanRevocationState(chanBucket kvdb.RwBucket, channel *OpenChannel) erro } func readChanConfig(b io.Reader, c *ChannelConfig) error { - return ReadElements(b, - &c.DustLimit, &c.MaxPendingAmount, &c.ChanReserve, - &c.MinHTLC, &c.MaxAcceptedHtlcs, &c.CsvDelay, - &c.MultiSigKey, &c.RevocationBasePoint, - &c.PaymentBasePoint, &c.DelayBasePoint, - &c.HtlcBasePoint, - ) + return cstate.ReadChanConfig(b, c) } func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { diff --git a/chanstate/config.go b/chanstate/config.go index e9adecf258..398cc6e16c 100644 --- a/chanstate/config.go +++ b/chanstate/config.go @@ -1,6 +1,8 @@ package chanstate import ( + "io" + "github.com/btcsuite/btcd/btcutil/v2" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" @@ -106,3 +108,24 @@ type ChannelConfig struct { // within any HTLC output scripts. HtlcBasePoint keychain.KeyDescriptor } + +// WriteChanConfig serializes a channel config. +func WriteChanConfig(b io.Writer, c *ChannelConfig) error { + return WriteElements(b, + c.DustLimit, c.MaxPendingAmount, c.ChanReserve, c.MinHTLC, + c.MaxAcceptedHtlcs, c.CsvDelay, c.MultiSigKey, + c.RevocationBasePoint, c.PaymentBasePoint, c.DelayBasePoint, + c.HtlcBasePoint, + ) +} + +// ReadChanConfig deserializes a channel config. +func ReadChanConfig(b io.Reader, c *ChannelConfig) error { + return ReadElements(b, + &c.DustLimit, &c.MaxPendingAmount, &c.ChanReserve, + &c.MinHTLC, &c.MaxAcceptedHtlcs, &c.CsvDelay, + &c.MultiSigKey, &c.RevocationBasePoint, + &c.PaymentBasePoint, &c.DelayBasePoint, + &c.HtlcBasePoint, + ) +} From f9ff9ff21821524d79151d90d85a6bee2ddfbd95 Mon Sep 17 00:00:00 2001 From: ziggie Date: Sun, 17 May 2026 23:55:44 -0300 Subject: [PATCH 21/61] chanstate: group commitment kv codecs Move the commitment, HTLC, commit-diff, and commitment auxiliary TLV codecs into a single kv_commitment file. This keeps storage serialization grouped by the commitment domain while preserving the kv_ prefix for backend-specific on-disk helpers. --- chanstate/commit_diff_serialization.go | 127 -------- chanstate/commit_tlv_data.go | 94 ------ chanstate/commitment_serialization.go | 39 --- chanstate/htlc_serialization.go | 191 ----------- chanstate/kv_commitment.go | 434 +++++++++++++++++++++++++ 5 files changed, 434 insertions(+), 451 deletions(-) delete mode 100644 chanstate/commit_diff_serialization.go delete mode 100644 chanstate/commit_tlv_data.go delete mode 100644 chanstate/commitment_serialization.go delete mode 100644 chanstate/htlc_serialization.go create mode 100644 chanstate/kv_commitment.go diff --git a/chanstate/commit_diff_serialization.go b/chanstate/commit_diff_serialization.go deleted file mode 100644 index f34a9f6804..0000000000 --- a/chanstate/commit_diff_serialization.go +++ /dev/null @@ -1,127 +0,0 @@ -package chanstate - -import ( - "encoding/binary" - "fmt" - "io" - - "github.com/lightningnetwork/lnd/graph/db/models" - "github.com/lightningnetwork/lnd/lnwire" -) - -// SerializeCommitDiff serializes the commit diff. -func SerializeCommitDiff(w io.Writer, diff *CommitDiff) error { - if err := SerializeChanCommit(w, &diff.Commitment); err != nil { - return err - } - - if err := WriteElements(w, diff.CommitSig); err != nil { - return err - } - - if err := SerializeLogUpdates(w, diff.LogUpdates); err != nil { - return err - } - - numOpenRefs := uint16(len(diff.OpenedCircuitKeys)) - if err := binary.Write(w, byteOrder, numOpenRefs); err != nil { - return err - } - - for _, openRef := range diff.OpenedCircuitKeys { - err := WriteElements(w, openRef.ChanID, openRef.HtlcID) - if err != nil { - return err - } - } - - numClosedRefs := uint16(len(diff.ClosedCircuitKeys)) - if err := binary.Write(w, byteOrder, numClosedRefs); err != nil { - return err - } - - for _, closedRef := range diff.ClosedCircuitKeys { - err := WriteElements(w, closedRef.ChanID, closedRef.HtlcID) - if err != nil { - return err - } - } - - // We'll also encode the commit aux data stream here. We do this here - // rather than above (at the call to serializeChanCommit), to ensure - // backwards compat for reads to existing non-custom channels. - if err := EncodeCommitTlvData(w, &diff.Commitment); err != nil { - return fmt.Errorf("unable to write aux data: %w", err) - } - - return nil -} - -// DeserializeCommitDiff deserializes the commit diff. -func DeserializeCommitDiff(r io.Reader) (*CommitDiff, error) { - var ( - d CommitDiff - err error - ) - - d.Commitment, err = DeserializeChanCommit(r) - if err != nil { - return nil, err - } - - var msg lnwire.Message - if err := ReadElements(r, &msg); err != nil { - return nil, err - } - commitSig, ok := msg.(*lnwire.CommitSig) - if !ok { - return nil, fmt.Errorf("expected lnwire.CommitSig, instead "+ - "read: %T", msg) - } - d.CommitSig = commitSig - - d.LogUpdates, err = DeserializeLogUpdates(r) - if err != nil { - return nil, err - } - - var numOpenRefs uint16 - if err := binary.Read(r, byteOrder, &numOpenRefs); err != nil { - return nil, err - } - - d.OpenedCircuitKeys = make([]models.CircuitKey, numOpenRefs) - for i := 0; i < int(numOpenRefs); i++ { - err := ReadElements(r, - &d.OpenedCircuitKeys[i].ChanID, - &d.OpenedCircuitKeys[i].HtlcID) - if err != nil { - return nil, err - } - } - - var numClosedRefs uint16 - if err := binary.Read(r, byteOrder, &numClosedRefs); err != nil { - return nil, err - } - - d.ClosedCircuitKeys = make([]models.CircuitKey, numClosedRefs) - for i := 0; i < int(numClosedRefs); i++ { - err := ReadElements(r, - &d.ClosedCircuitKeys[i].ChanID, - &d.ClosedCircuitKeys[i].HtlcID) - if err != nil { - return nil, err - } - } - - // As a final step, we'll read out any aux commit data that we have at - // the end of this byte stream. We do this here to ensure backward - // compatibility, as otherwise we risk erroneously reading into the - // wrong field. - if err := DecodeCommitTlvData(r, &d.Commitment); err != nil { - return nil, fmt.Errorf("unable to decode aux data: %w", err) - } - - return &d, nil -} diff --git a/chanstate/commit_tlv_data.go b/chanstate/commit_tlv_data.go deleted file mode 100644 index e3bee99f54..0000000000 --- a/chanstate/commit_tlv_data.go +++ /dev/null @@ -1,94 +0,0 @@ -package chanstate - -import ( - "io" - - "github.com/lightningnetwork/lnd/fn/v2" - "github.com/lightningnetwork/lnd/tlv" -) - -// commitTlvData stores all the optional data that may be stored as a TLV stream -// at the _end_ of the normal serialized commit on disk. -type commitTlvData struct { - // customBlob is a custom blob that may store extra data for custom - // channels. - customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob] -} - -// encode encodes the aux data into the passed io.Writer. -func (c *commitTlvData) encode(w io.Writer) error { - var tlvRecords []tlv.Record - c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) { - tlvRecords = append(tlvRecords, blob.Record()) - }) - - // Create the tlv stream. - tlvStream, err := tlv.NewStream(tlvRecords...) - if err != nil { - return err - } - - return tlvStream.Encode(w) -} - -// decode attempts to decode the aux data from the passed io.Reader. -func (c *commitTlvData) decode(r io.Reader) error { - blob := c.customBlob.Zero() - - tlvStream, err := tlv.NewStream( - blob.Record(), - ) - if err != nil { - return err - } - - tlvs, err := tlvStream.DecodeWithParsedTypes(r) - if err != nil { - return err - } - - if _, ok := tlvs[c.customBlob.TlvType()]; ok { - c.customBlob = tlv.SomeRecordT(blob) - } - - return nil -} - -// DecodeCommitTlvData decodes and applies auxiliary TLV data to a commitment. -func DecodeCommitTlvData(r io.Reader, c *ChannelCommitment) error { - var auxData commitTlvData - if err := auxData.decode(r); err != nil { - return err - } - - amendCommitTlvData(c, auxData) - - return nil -} - -// EncodeCommitTlvData extracts and encodes auxiliary TLV data from a -// commitment. -func EncodeCommitTlvData(w io.Writer, c *ChannelCommitment) error { - auxData := extractCommitTlvData(c) - return auxData.encode(w) -} - -// amendCommitTlvData updates the commitment with the given auxiliary TLV data. -func amendCommitTlvData(c *ChannelCommitment, auxData commitTlvData) { - auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { - c.CustomBlob = fn.Some(blob) - }) -} - -// extractCommitTlvData creates a new commitTlvData from the given commitment. -func extractCommitTlvData(c *ChannelCommitment) commitTlvData { - var auxData commitTlvData - - c.CustomBlob.WhenSome(func(blob tlv.Blob) { - auxData.customBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType1](blob), - ) - }) - - return auxData -} diff --git a/chanstate/commitment_serialization.go b/chanstate/commitment_serialization.go deleted file mode 100644 index fc294b2ea5..0000000000 --- a/chanstate/commitment_serialization.go +++ /dev/null @@ -1,39 +0,0 @@ -package chanstate - -import "io" - -// SerializeChanCommit serializes the channel commitment. -func SerializeChanCommit(w io.Writer, c *ChannelCommitment) error { - if err := WriteElements(w, - c.CommitHeight, c.LocalLogIndex, c.LocalHtlcIndex, - c.RemoteLogIndex, c.RemoteHtlcIndex, c.LocalBalance, - c.RemoteBalance, c.CommitFee, c.FeePerKw, c.CommitTx, - c.CommitSig, - ); err != nil { - return err - } - - return SerializeHtlcs(w, c.Htlcs...) -} - -// DeserializeChanCommit deserializes the channel commitment. -func DeserializeChanCommit(r io.Reader) (ChannelCommitment, error) { - var c ChannelCommitment - - err := ReadElements(r, - &c.CommitHeight, &c.LocalLogIndex, &c.LocalHtlcIndex, - &c.RemoteLogIndex, &c.RemoteHtlcIndex, &c.LocalBalance, - &c.RemoteBalance, &c.CommitFee, &c.FeePerKw, &c.CommitTx, - &c.CommitSig, - ) - if err != nil { - return c, err - } - - c.Htlcs, err = DeserializeHtlcs(r) - if err != nil { - return c, err - } - - return c, nil -} diff --git a/chanstate/htlc_serialization.go b/chanstate/htlc_serialization.go deleted file mode 100644 index 9083c46490..0000000000 --- a/chanstate/htlc_serialization.go +++ /dev/null @@ -1,191 +0,0 @@ -package chanstate - -import ( - "io" - - "github.com/btcsuite/btcd/btcec/v2" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/tlv" -) - -// serializeHtlcExtraData encodes a TLV stream of extra data to be stored with a -// HTLC. It uses the update_add_htlc TLV types, because this is where extra -// data is passed with a HTLC. At present blinding points are the only extra -// data that we will store, and the function is a no-op if a nil blinding -// point is provided. -// -// This function MUST be called to persist all HTLC values when they are -// serialized. -func serializeHtlcExtraData(h *HTLC) error { - var records []tlv.RecordProducer - h.BlindingPoint.WhenSome(func(b tlv.RecordT[lnwire.BlindingPointTlvType, - *btcec.PublicKey]) { - - records = append(records, &b) - }) - - records, err := h.CustomRecords.ExtendRecordProducers(records) - if err != nil { - return err - } - - return h.ExtraData.PackRecords(records...) -} - -// deserializeHtlcExtraData extracts TLVs from the extra data persisted for the -// HTLC and populates values in the struct accordingly. -// -// This function MUST be called to populate the struct properly when HTLCs -// are deserialized. -func deserializeHtlcExtraData(h *HTLC) error { - if len(h.ExtraData) == 0 { - return nil - } - - blindingPoint := h.BlindingPoint.Zero() - tlvMap, err := h.ExtraData.ExtractRecords(&blindingPoint) - if err != nil { - return err - } - - if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil { - h.BlindingPoint = tlv.SomeRecordT(blindingPoint) - - // Remove the entry from the TLV map. Anything left in the map - // will be included in the custom records field. - delete(tlvMap, h.BlindingPoint.TlvType()) - } - - // Set the custom records field to the remaining TLV records. - customRecords, err := lnwire.NewCustomRecords(tlvMap) - if err != nil { - return err - } - h.CustomRecords = customRecords - - return nil -} - -// SerializeHtlcs writes out the passed set of HTLC's into the passed writer -// using the current default on-disk serialization format. -// -// This inline serialization has been extended to allow storage of extra data -// associated with a HTLC in the following way: -// - The known-length onion blob (1366 bytes) is serialized as var bytes in -// WriteElements (ie, the length 1366 was written, followed by the 1366 -// onion bytes). -// - To include extra data, we append any extra data present to this one -// variable length of data. Since we know that the onion is strictly 1366 -// bytes, any length after that should be considered to be extra data. -// -// NOTE: This API is NOT stable, the on-disk format will likely change in the -// future. -func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error { - numHtlcs := uint16(len(htlcs)) - if err := WriteElement(b, numHtlcs); err != nil { - return err - } - - for _, htlc := range htlcs { - // Populate TLV stream for any additional fields contained - // in the TLV. - if err := serializeHtlcExtraData(&htlc); err != nil { - return err - } - - // The onion blob and hltc data are stored as a single var - // bytes blob. - onionAndExtraData := make( - []byte, lnwire.OnionPacketSize+len(htlc.ExtraData), - ) - copy(onionAndExtraData, htlc.OnionBlob[:]) - copy(onionAndExtraData[lnwire.OnionPacketSize:], htlc.ExtraData) - - if err := WriteElements(b, - //nolint:ll - htlc.Signature, htlc.RHash, htlc.Amt, htlc.RefundTimeout, - htlc.OutputIndex, htlc.Incoming, onionAndExtraData, - htlc.HtlcIndex, htlc.LogIndex, - ); err != nil { - return err - } - } - - return nil -} - -// DeserializeHtlcs attempts to read out a slice of HTLC's from the passed -// io.Reader. The bytes within the passed reader MUST have been previously -// written to using the SerializeHtlcs function. -// -// This inline deserialization has been extended to allow storage of extra data -// associated with a HTLC in the following way: -// - The known-length onion blob (1366 bytes) and any additional data present -// are read out as a single blob of variable byte data. -// - They are stored like this to take advantage of the variable space -// available for extension without migration (see SerializeHtlcs). -// - The first 1366 bytes are interpreted as the onion blob, and any remaining -// bytes as extra HTLC data. -// - This extra HTLC data is expected to be serialized as a TLV stream, and -// its parsing is left to higher layers. -// -// NOTE: This API is NOT stable, the on-disk format will likely change in the -// future. -func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { - var numHtlcs uint16 - if err := ReadElement(r, &numHtlcs); err != nil { - return nil, err - } - - var htlcs []HTLC - if numHtlcs == 0 { - return htlcs, nil - } - - htlcs = make([]HTLC, numHtlcs) - for i := uint16(0); i < numHtlcs; i++ { - var onionAndExtraData []byte - if err := ReadElements(r, - &htlcs[i].Signature, &htlcs[i].RHash, &htlcs[i].Amt, - &htlcs[i].RefundTimeout, &htlcs[i].OutputIndex, - &htlcs[i].Incoming, &onionAndExtraData, - &htlcs[i].HtlcIndex, &htlcs[i].LogIndex, - ); err != nil { - return htlcs, err - } - - // Sanity check that we have at least the onion blob size we - // expect. - if len(onionAndExtraData) < lnwire.OnionPacketSize { - return nil, ErrOnionBlobLength - } - - // First OnionPacketSize bytes are our fixed length onion - // packet. - copy( - htlcs[i].OnionBlob[:], - onionAndExtraData[0:lnwire.OnionPacketSize], - ) - - // Any additional bytes belong to extra data. ExtraDataLen - // will be >= 0, because we know that we always have a fixed - // length onion packet. - extraDataLen := len(onionAndExtraData) - lnwire.OnionPacketSize - if extraDataLen > 0 { - htlcs[i].ExtraData = make([]byte, extraDataLen) - - copy( - htlcs[i].ExtraData, - onionAndExtraData[lnwire.OnionPacketSize:], - ) - } - - // Finally, deserialize any TLVs contained in that extra data - // if they are present. - if err := deserializeHtlcExtraData(&htlcs[i]); err != nil { - return nil, err - } - } - - return htlcs, nil -} diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go new file mode 100644 index 0000000000..11fe5ef094 --- /dev/null +++ b/chanstate/kv_commitment.go @@ -0,0 +1,434 @@ +package chanstate + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// serializeHtlcExtraData encodes a TLV stream of extra data to be stored with a +// HTLC. It uses the update_add_htlc TLV types, because this is where extra +// data is passed with a HTLC. At present blinding points are the only extra +// data that we will store, and the function is a no-op if a nil blinding +// point is provided. +// +// This function MUST be called to persist all HTLC values when they are +// serialized. +func serializeHtlcExtraData(h *HTLC) error { + var records []tlv.RecordProducer + h.BlindingPoint.WhenSome(func(b tlv.RecordT[lnwire.BlindingPointTlvType, + *btcec.PublicKey]) { + + records = append(records, &b) + }) + + records, err := h.CustomRecords.ExtendRecordProducers(records) + if err != nil { + return err + } + + return h.ExtraData.PackRecords(records...) +} + +// deserializeHtlcExtraData extracts TLVs from the extra data persisted for the +// HTLC and populates values in the struct accordingly. +// +// This function MUST be called to populate the struct properly when HTLCs +// are deserialized. +func deserializeHtlcExtraData(h *HTLC) error { + if len(h.ExtraData) == 0 { + return nil + } + + blindingPoint := h.BlindingPoint.Zero() + tlvMap, err := h.ExtraData.ExtractRecords(&blindingPoint) + if err != nil { + return err + } + + if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil { + h.BlindingPoint = tlv.SomeRecordT(blindingPoint) + + // Remove the entry from the TLV map. Anything left in the map + // will be included in the custom records field. + delete(tlvMap, h.BlindingPoint.TlvType()) + } + + // Set the custom records field to the remaining TLV records. + customRecords, err := lnwire.NewCustomRecords(tlvMap) + if err != nil { + return err + } + h.CustomRecords = customRecords + + return nil +} + +// SerializeHtlcs writes out the passed set of HTLC's into the passed writer +// using the current default on-disk serialization format. +// +// This inline serialization has been extended to allow storage of extra data +// associated with a HTLC in the following way: +// - The known-length onion blob (1366 bytes) is serialized as var bytes in +// WriteElements (ie, the length 1366 was written, followed by the 1366 +// onion bytes). +// - To include extra data, we append any extra data present to this one +// variable length of data. Since we know that the onion is strictly 1366 +// bytes, any length after that should be considered to be extra data. +// +// NOTE: This API is NOT stable, the on-disk format will likely change in the +// future. +func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error { + numHtlcs := uint16(len(htlcs)) + if err := WriteElement(b, numHtlcs); err != nil { + return err + } + + for _, htlc := range htlcs { + // Populate TLV stream for any additional fields contained + // in the TLV. + if err := serializeHtlcExtraData(&htlc); err != nil { + return err + } + + // The onion blob and hltc data are stored as a single var + // bytes blob. + onionAndExtraData := make( + []byte, lnwire.OnionPacketSize+len(htlc.ExtraData), + ) + copy(onionAndExtraData, htlc.OnionBlob[:]) + copy(onionAndExtraData[lnwire.OnionPacketSize:], htlc.ExtraData) + + if err := WriteElements(b, + //nolint:ll + htlc.Signature, htlc.RHash, htlc.Amt, htlc.RefundTimeout, + htlc.OutputIndex, htlc.Incoming, onionAndExtraData, + htlc.HtlcIndex, htlc.LogIndex, + ); err != nil { + return err + } + } + + return nil +} + +// DeserializeHtlcs attempts to read out a slice of HTLC's from the passed +// io.Reader. The bytes within the passed reader MUST have been previously +// written to using the SerializeHtlcs function. +// +// This inline deserialization has been extended to allow storage of extra data +// associated with a HTLC in the following way: +// - The known-length onion blob (1366 bytes) and any additional data present +// are read out as a single blob of variable byte data. +// - They are stored like this to take advantage of the variable space +// available for extension without migration (see SerializeHtlcs). +// - The first 1366 bytes are interpreted as the onion blob, and any remaining +// bytes as extra HTLC data. +// - This extra HTLC data is expected to be serialized as a TLV stream, and +// its parsing is left to higher layers. +// +// NOTE: This API is NOT stable, the on-disk format will likely change in the +// future. +func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { + var numHtlcs uint16 + if err := ReadElement(r, &numHtlcs); err != nil { + return nil, err + } + + var htlcs []HTLC + if numHtlcs == 0 { + return htlcs, nil + } + + htlcs = make([]HTLC, numHtlcs) + for i := uint16(0); i < numHtlcs; i++ { + var onionAndExtraData []byte + if err := ReadElements(r, + &htlcs[i].Signature, &htlcs[i].RHash, &htlcs[i].Amt, + &htlcs[i].RefundTimeout, &htlcs[i].OutputIndex, + &htlcs[i].Incoming, &onionAndExtraData, + &htlcs[i].HtlcIndex, &htlcs[i].LogIndex, + ); err != nil { + return htlcs, err + } + + // Sanity check that we have at least the onion blob size we + // expect. + if len(onionAndExtraData) < lnwire.OnionPacketSize { + return nil, ErrOnionBlobLength + } + + // First OnionPacketSize bytes are our fixed length onion + // packet. + copy( + htlcs[i].OnionBlob[:], + onionAndExtraData[0:lnwire.OnionPacketSize], + ) + + // Any additional bytes belong to extra data. ExtraDataLen + // will be >= 0, because we know that we always have a fixed + // length onion packet. + extraDataLen := len(onionAndExtraData) - lnwire.OnionPacketSize + if extraDataLen > 0 { + htlcs[i].ExtraData = make([]byte, extraDataLen) + + copy( + htlcs[i].ExtraData, + onionAndExtraData[lnwire.OnionPacketSize:], + ) + } + + // Finally, deserialize any TLVs contained in that extra data + // if they are present. + if err := deserializeHtlcExtraData(&htlcs[i]); err != nil { + return nil, err + } + } + + return htlcs, nil +} + +// SerializeChanCommit serializes the channel commitment. +func SerializeChanCommit(w io.Writer, c *ChannelCommitment) error { + if err := WriteElements(w, + c.CommitHeight, c.LocalLogIndex, c.LocalHtlcIndex, + c.RemoteLogIndex, c.RemoteHtlcIndex, c.LocalBalance, + c.RemoteBalance, c.CommitFee, c.FeePerKw, c.CommitTx, + c.CommitSig, + ); err != nil { + return err + } + + return SerializeHtlcs(w, c.Htlcs...) +} + +// DeserializeChanCommit deserializes the channel commitment. +func DeserializeChanCommit(r io.Reader) (ChannelCommitment, error) { + var c ChannelCommitment + + err := ReadElements(r, + &c.CommitHeight, &c.LocalLogIndex, &c.LocalHtlcIndex, + &c.RemoteLogIndex, &c.RemoteHtlcIndex, &c.LocalBalance, + &c.RemoteBalance, &c.CommitFee, &c.FeePerKw, &c.CommitTx, + &c.CommitSig, + ) + if err != nil { + return c, err + } + + c.Htlcs, err = DeserializeHtlcs(r) + if err != nil { + return c, err + } + + return c, nil +} + +// commitTlvData stores all the optional data that may be stored as a TLV stream +// at the _end_ of the normal serialized commit on disk. +type commitTlvData struct { + // customBlob is a custom blob that may store extra data for custom + // channels. + customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob] +} + +// encode encodes the aux data into the passed io.Writer. +func (c *commitTlvData) encode(w io.Writer) error { + var tlvRecords []tlv.Record + c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) { + tlvRecords = append(tlvRecords, blob.Record()) + }) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// decode attempts to decode the aux data from the passed io.Reader. +func (c *commitTlvData) decode(r io.Reader) error { + blob := c.customBlob.Zero() + + tlvStream, err := tlv.NewStream( + blob.Record(), + ) + if err != nil { + return err + } + + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[c.customBlob.TlvType()]; ok { + c.customBlob = tlv.SomeRecordT(blob) + } + + return nil +} + +// DecodeCommitTlvData decodes and applies auxiliary TLV data to a commitment. +func DecodeCommitTlvData(r io.Reader, c *ChannelCommitment) error { + var auxData commitTlvData + if err := auxData.decode(r); err != nil { + return err + } + + amendCommitTlvData(c, auxData) + + return nil +} + +// EncodeCommitTlvData extracts and encodes auxiliary TLV data from a +// commitment. +func EncodeCommitTlvData(w io.Writer, c *ChannelCommitment) error { + auxData := extractCommitTlvData(c) + return auxData.encode(w) +} + +// amendCommitTlvData updates the commitment with the given auxiliary TLV data. +func amendCommitTlvData(c *ChannelCommitment, auxData commitTlvData) { + auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { + c.CustomBlob = fn.Some(blob) + }) +} + +// extractCommitTlvData creates a new commitTlvData from the given commitment. +func extractCommitTlvData(c *ChannelCommitment) commitTlvData { + var auxData commitTlvData + + c.CustomBlob.WhenSome(func(blob tlv.Blob) { + auxData.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType1](blob), + ) + }) + + return auxData +} + +// SerializeCommitDiff serializes the commit diff. +func SerializeCommitDiff(w io.Writer, diff *CommitDiff) error { + if err := SerializeChanCommit(w, &diff.Commitment); err != nil { + return err + } + + if err := WriteElements(w, diff.CommitSig); err != nil { + return err + } + + if err := SerializeLogUpdates(w, diff.LogUpdates); err != nil { + return err + } + + numOpenRefs := uint16(len(diff.OpenedCircuitKeys)) + if err := binary.Write(w, byteOrder, numOpenRefs); err != nil { + return err + } + + for _, openRef := range diff.OpenedCircuitKeys { + err := WriteElements(w, openRef.ChanID, openRef.HtlcID) + if err != nil { + return err + } + } + + numClosedRefs := uint16(len(diff.ClosedCircuitKeys)) + if err := binary.Write(w, byteOrder, numClosedRefs); err != nil { + return err + } + + for _, closedRef := range diff.ClosedCircuitKeys { + err := WriteElements(w, closedRef.ChanID, closedRef.HtlcID) + if err != nil { + return err + } + } + + // We'll also encode the commit aux data stream here. We do this here + // rather than above (at the call to serializeChanCommit), to ensure + // backwards compat for reads to existing non-custom channels. + if err := EncodeCommitTlvData(w, &diff.Commitment); err != nil { + return fmt.Errorf("unable to write aux data: %w", err) + } + + return nil +} + +// DeserializeCommitDiff deserializes the commit diff. +func DeserializeCommitDiff(r io.Reader) (*CommitDiff, error) { + var ( + d CommitDiff + err error + ) + + d.Commitment, err = DeserializeChanCommit(r) + if err != nil { + return nil, err + } + + var msg lnwire.Message + if err := ReadElements(r, &msg); err != nil { + return nil, err + } + commitSig, ok := msg.(*lnwire.CommitSig) + if !ok { + return nil, fmt.Errorf("expected lnwire.CommitSig, instead "+ + "read: %T", msg) + } + d.CommitSig = commitSig + + d.LogUpdates, err = DeserializeLogUpdates(r) + if err != nil { + return nil, err + } + + var numOpenRefs uint16 + if err := binary.Read(r, byteOrder, &numOpenRefs); err != nil { + return nil, err + } + + d.OpenedCircuitKeys = make([]models.CircuitKey, numOpenRefs) + for i := 0; i < int(numOpenRefs); i++ { + err := ReadElements(r, + &d.OpenedCircuitKeys[i].ChanID, + &d.OpenedCircuitKeys[i].HtlcID) + if err != nil { + return nil, err + } + } + + var numClosedRefs uint16 + if err := binary.Read(r, byteOrder, &numClosedRefs); err != nil { + return nil, err + } + + d.ClosedCircuitKeys = make([]models.CircuitKey, numClosedRefs) + for i := 0; i < int(numClosedRefs); i++ { + err := ReadElements(r, + &d.ClosedCircuitKeys[i].ChanID, + &d.ClosedCircuitKeys[i].HtlcID) + if err != nil { + return nil, err + } + } + + // As a final step, we'll read out any aux commit data that we have at + // the end of this byte stream. We do this here to ensure backward + // compatibility, as otherwise we risk erroneously reading into the + // wrong field. + if err := DecodeCommitTlvData(r, &d.Commitment); err != nil { + return nil, fmt.Errorf("unable to decode aux data: %w", err) + } + + return &d, nil +} From 0ee6c6ad72bea813bf21562f2b531a40fc4e4d78 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 00:06:22 -0300 Subject: [PATCH 22/61] chanstate: move kv serialization to stores Move the generic log update serialization helpers into the KV store files that use them. Keep the shared byte order with the codec, place commit-diff log update list serialization with commitment storage, and give forwarding and revocation logs their own key helpers. --- chanstate/codec.go | 2 + chanstate/kv_commitment.go | 37 ++++ chanstate/kv_forwarding_package.go | 51 +++-- chanstate/kv_revocation_log.go | 315 ++--------------------------- chanstate/kv_serialization.go | 67 ------ chanstate/revocation_log.go | 296 +++++++++++++++++++++++++++ 6 files changed, 384 insertions(+), 384 deletions(-) delete mode 100644 chanstate/kv_serialization.go diff --git a/chanstate/codec.go b/chanstate/codec.go index 36333733c0..021c88f989 100644 --- a/chanstate/codec.go +++ b/chanstate/codec.go @@ -18,6 +18,8 @@ import ( "github.com/lightningnetwork/lnd/tlv" ) +var byteOrder = binary.BigEndian + // UnknownElementType is an error returned when the codec is unable to encode or // decode a particular type. type UnknownElementType struct { diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go index 11fe5ef094..1ca51f7b2c 100644 --- a/chanstate/kv_commitment.go +++ b/chanstate/kv_commitment.go @@ -316,6 +316,43 @@ func extractCommitTlvData(c *ChannelCommitment) commitTlvData { return auxData } +// SerializeLogUpdates serializes provided list of updates to a stream. +func SerializeLogUpdates(w io.Writer, logUpdates []LogUpdate) error { + numUpdates := uint16(len(logUpdates)) + if err := binary.Write(w, byteOrder, numUpdates); err != nil { + return err + } + + for _, diff := range logUpdates { + err := WriteElements(w, diff.LogIndex, diff.UpdateMsg) + if err != nil { + return err + } + } + + return nil +} + +// DeserializeLogUpdates deserializes a list of updates from a stream. +func DeserializeLogUpdates(r io.Reader) ([]LogUpdate, error) { + var numUpdates uint16 + if err := binary.Read(r, byteOrder, &numUpdates); err != nil { + return nil, err + } + + logUpdates := make([]LogUpdate, numUpdates) + for i := 0; i < int(numUpdates); i++ { + err := ReadElements(r, + &logUpdates[i].LogIndex, &logUpdates[i].UpdateMsg, + ) + if err != nil { + return nil, err + } + } + + return logUpdates, nil +} + // SerializeCommitDiff serializes the commit diff. func SerializeCommitDiff(w io.Writer, diff *CommitDiff) error { if err := SerializeChanCommit(w, &diff.Commitment); err != nil { diff --git a/chanstate/kv_forwarding_package.go b/chanstate/kv_forwarding_package.go index c740ebc10c..f6e94f180b 100644 --- a/chanstate/kv_forwarding_package.go +++ b/chanstate/kv_forwarding_package.go @@ -3,6 +3,7 @@ package chanstate import ( "bytes" "errors" + "io" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -211,13 +212,13 @@ func (*ChannelPackager) AddFwdPkg(tx kvdb.RwTx, fwdPkg *FwdPkg) error { return err } - source := makeLogKey(fwdPkg.Source.ToUint64()) + source := forwardingLogKey(fwdPkg.Source.ToUint64()) sourceBkt, err := fwdPkgBkt.CreateBucketIfNotExists(source[:]) if err != nil { return err } - heightKey := makeLogKey(fwdPkg.Height) + heightKey := forwardingLogKey(fwdPkg.Height) heightBkt, err := sourceBkt.CreateBucketIfNotExists(heightKey[:]) if err != nil { return err @@ -284,6 +285,21 @@ func putLogUpdate(bkt kvdb.RwBucket, idx uint16, htlc *LogUpdate) error { return bkt.Put(uint16Key(idx), b.Bytes()) } +// serializeLogUpdate writes a log update to the provided io.Writer. +func serializeLogUpdate(w io.Writer, l *LogUpdate) error { + return WriteElements(w, l.LogIndex, l.UpdateMsg) +} + +// deserializeLogUpdate reads a log update from the provided io.Reader. +func deserializeLogUpdate(r io.Reader) (*LogUpdate, error) { + l := &LogUpdate{} + if err := ReadElements(r, &l.LogIndex, &l.UpdateMsg); err != nil { + return nil, err + } + + return l, nil +} + // LoadFwdPkgs scans the forwarding log for any packages that haven't been // processed, and returns their deserialized log updates in a map indexed by the // remote commitment height at which the updates were locked in. @@ -300,7 +316,7 @@ func loadChannelFwdPkgs(tx kvdb.RTx, return nil, nil } - sourceKey := makeLogKey(source.ToUint64()) + sourceKey := forwardingLogKey(source.ToUint64()) sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) if sourceBkt == nil { return nil, nil @@ -338,13 +354,13 @@ func loadChannelFwdPkgs(tx kvdb.RTx, func loadFwdPkg(fwdPkgBkt kvdb.RBucket, source lnwire.ShortChannelID, height uint64) (*FwdPkg, error) { - sourceKey := makeLogKey(source.ToUint64()) + sourceKey := forwardingLogKey(source.ToUint64()) sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) if sourceBkt == nil { return nil, ErrCorruptedFwdPkg } - heightKey := makeLogKey(height) + heightKey := forwardingLogKey(height) heightBkt := sourceBkt.NestedReadBucket(heightKey[:]) if heightBkt == nil { return nil, ErrCorruptedFwdPkg @@ -481,13 +497,13 @@ func (p *ChannelPackager) SetFwdFilter(tx kvdb.RwTx, height uint64, return ErrCorruptedFwdPkg } - source := makeLogKey(p.source.ToUint64()) + source := forwardingLogKey(p.source.ToUint64()) sourceBkt := fwdPkgBkt.NestedReadWriteBucket(source[:]) if sourceBkt == nil { return ErrCorruptedFwdPkg } - heightKey := makeLogKey(height) + heightKey := forwardingLogKey(height) heightBkt := sourceBkt.NestedReadWriteBucket(heightKey[:]) if heightBkt == nil { return ErrCorruptedFwdPkg @@ -522,7 +538,7 @@ func (p *ChannelPackager) AckAddHtlcs(tx kvdb.RwTx, addRefs ...AddRef) error { return ErrCorruptedFwdPkg } - sourceKey := makeLogKey(p.source.ToUint64()) + sourceKey := forwardingLogKey(p.source.ToUint64()) sourceBkt := fwdPkgBkt.NestedReadWriteBucket(sourceKey[:]) if sourceBkt == nil { return ErrCorruptedFwdPkg @@ -555,7 +571,7 @@ func (p *ChannelPackager) AckAddHtlcs(tx kvdb.RwTx, addRefs ...AddRef) error { func ackAddHtlcsAtHeight(sourceBkt kvdb.RwBucket, height uint64, indexes []uint16) error { - heightKey := makeLogKey(height) + heightKey := forwardingLogKey(height) heightBkt := sourceBkt.NestedReadWriteBucket(heightKey[:]) if heightBkt == nil { // If the height bucket isn't found, this could be because the @@ -632,7 +648,7 @@ func ackSettleFails(tx kvdb.RwTx, settleFailRefs []SettleFailRef) error { // each remote bucket, and update the settle fail filter for any // settle/fail htlcs. for dest, destHeights := range destHeightDiffs { - destKey := makeLogKey(dest.ToUint64()) + destKey := forwardingLogKey(dest.ToUint64()) destBkt := fwdPkgBkt.NestedReadWriteBucket(destKey[:]) if destBkt == nil { // If the destination bucket is not found, this is @@ -659,7 +675,7 @@ func ackSettleFails(tx kvdb.RwTx, settleFailRefs []SettleFailRef) error { func ackSettleFailsAtHeight(destBkt kvdb.RwBucket, height uint64, indexes []uint16) error { - heightKey := makeLogKey(height) + heightKey := forwardingLogKey(height) heightBkt := destBkt.NestedReadWriteBucket(heightKey[:]) if heightBkt == nil { // If the height bucket isn't found, this could be because the @@ -702,13 +718,13 @@ func (p *ChannelPackager) RemovePkg(tx kvdb.RwTx, height uint64) error { return nil } - sourceBytes := makeLogKey(p.source.ToUint64()) + sourceBytes := forwardingLogKey(p.source.ToUint64()) sourceBkt := fwdPkgBkt.NestedReadWriteBucket(sourceBytes[:]) if sourceBkt == nil { return ErrCorruptedFwdPkg } - heightKey := makeLogKey(height) + heightKey := forwardingLogKey(height) return sourceBkt.DeleteNestedBucket(heightKey[:]) } @@ -721,7 +737,7 @@ func (p *ChannelPackager) Wipe(tx kvdb.RwTx) error { return nil } - sourceBytes := makeLogKey(p.source.ToUint64()) + sourceBytes := forwardingLogKey(p.source.ToUint64()) // If the nested bucket doesn't exist, there's no need to delete. if fwdPkgBkt.NestedReadWriteBucket(sourceBytes[:]) == nil { @@ -738,6 +754,13 @@ func uint16Key(i uint16) []byte { return key } +// forwardingLogKey converts a uint64 into an 8 byte forwarding package key. +func forwardingLogKey(updateNum uint64) [8]byte { + var key [8]byte + byteOrder.PutUint64(key[:], updateNum) + return key +} + // Compile-time constraint to ensure that ChannelPackager implements the public // FwdPackager interface. var _ FwdPackager = (*ChannelPackager)(nil) diff --git a/chanstate/kv_revocation_log.go b/chanstate/kv_revocation_log.go index 30124f51db..bae42a3a93 100644 --- a/chanstate/kv_revocation_log.go +++ b/chanstate/kv_revocation_log.go @@ -2,18 +2,13 @@ package chanstate import ( "bytes" - "encoding/binary" "errors" - "io" "math" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/tlv" ) -// This file contains the KV/TLV serialization helpers for revocation logs. -// The domain types remain in revocation_log.go. - var ( // revocationLogBucket is a sub-bucket under openChannelBucket. This // sub-bucket is dedicated for storing the minimal info required to @@ -37,9 +32,9 @@ func RevocationLogBucketKey() []byte { } // PutRevocationLog uses the fields `CommitTx` and `Htlcs` from a -// ChannelCommitment to construct a revocation log entry and saves them to disk. -// It also saves our output index and their output index, which are useful when -// creating breach retribution. +// ChannelCommitment to construct a revocation log entry and saves them to +// disk. It also saves our output index and their output index, which are +// useful when creating breach retribution. func PutRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, ourOutputIndex, theirOutputIndex uint32, noAmtData bool) error { @@ -86,7 +81,8 @@ func PutRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, continue } - // Sanity check that the output indexes can be safely converted. + // Sanity check that the output indexes can be safely + // converted. if htlc.OutputIndex > math.MaxUint16 { return ErrOutputIndexTooBig } @@ -104,7 +100,7 @@ func PutRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, return err } - logEntrykey := makeLogKey(commit.CommitHeight) + logEntrykey := revocationLogKey(commit.CommitHeight) return bucket.Put(logEntrykey[:], b.Bytes()) } @@ -114,7 +110,7 @@ func PutRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, func FetchRevocationLog(log kvdb.RBucket, updateNum uint64) (RevocationLog, error) { - logEntrykey := makeLogKey(updateNum) + logEntrykey := revocationLogKey(updateNum) commitBytes := log.Get(logEntrykey[:]) if commitBytes == nil { return RevocationLog{}, ErrLogEntryNotFound @@ -125,296 +121,9 @@ func FetchRevocationLog(log kvdb.RBucket, return DeserializeRevocationLog(commitReader) } -// htlcEntryToTlvStream converts an HTLCEntry record into a tlv representation. -func htlcEntryToTlvStream(h *HTLCEntry) (*tlv.Stream, error) { - records := []tlv.Record{ - h.RHash.Record(), - h.RefundTimeout.Record(), - h.OutputIndex.Record(), - h.Incoming.Record(), - h.Amt.Record(), - } - - h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { - records = append(records, r.Record()) - }) - - h.HtlcIndex.WhenSome(func(r tlv.RecordT[tlv.TlvType6, - tlv.BigSizeT[uint64]]) { - - records = append(records, r.Record()) - }) - - tlv.SortRecords(records) - - return tlv.NewStream(records...) -} - -// SerializeRevocationLog serializes a RevocationLog record based on tlv -// format. -func SerializeRevocationLog(w io.Writer, rl *RevocationLog) error { - // Add the tlv records for all non-optional fields. - records := []tlv.Record{ - rl.OurOutputIndex.Record(), - rl.TheirOutputIndex.Record(), - rl.CommitTxHash.Record(), - } - - // Now we add any optional fields that are non-nil. - rl.OurBalance.WhenSome( - func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) { - records = append(records, r.Record()) - }, - ) - - rl.TheirBalance.WhenSome( - func(r tlv.RecordT[tlv.TlvType4, BigSizeMilliSatoshi]) { - records = append(records, r.Record()) - }, - ) - - rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { - records = append(records, r.Record()) - }) - - // Create the tlv stream. - tlvStream, err := tlv.NewStream(records...) - if err != nil { - return err - } - - // Write the tlv stream. - if err := WriteTlvStream(w, tlvStream); err != nil { - return err - } - - // Write the HTLCs. - return SerializeHTLCEntries(w, rl.HTLCEntries) -} - -// SerializeHTLCEntries serializes a list of HTLCEntry records based on tlv -// format. -func SerializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { - for _, htlc := range htlcs { - // Create the tlv stream. - tlvStream, err := htlcEntryToTlvStream(htlc) - if err != nil { - return err - } - - // Write the tlv stream. - if err := WriteTlvStream(w, tlvStream); err != nil { - return err - } - } - - return nil -} - -// DeserializeRevocationLog deserializes a RevocationLog based on tlv format. -func DeserializeRevocationLog(r io.Reader) (RevocationLog, error) { - var rl RevocationLog - - ourBalance := rl.OurBalance.Zero() - theirBalance := rl.TheirBalance.Zero() - customBlob := rl.CustomBlob.Zero() - - // Create the tlv stream. - tlvStream, err := tlv.NewStream( - rl.OurOutputIndex.Record(), - rl.TheirOutputIndex.Record(), - rl.CommitTxHash.Record(), - ourBalance.Record(), - theirBalance.Record(), - customBlob.Record(), - ) - if err != nil { - return rl, err - } - - // Read the tlv stream. - parsedTypes, err := ReadTlvStream(r, tlvStream) - if err != nil { - return rl, err - } - - if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil { - rl.OurBalance = tlv.SomeRecordT(ourBalance) - } - - if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil { - rl.TheirBalance = tlv.SomeRecordT(theirBalance) - } - - if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { - rl.CustomBlob = tlv.SomeRecordT(customBlob) - } - - // Read the HTLC entries. - rl.HTLCEntries, err = DeserializeHTLCEntries(r) - - return rl, err -} - -// DeserializeHTLCEntries deserializes a list of HTLC entries based on tlv -// format. -func DeserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { - var ( - htlcs []*HTLCEntry - - // htlcIndexBlob defines the tlv record type to be used when - // decoding from the disk. We use it instead of the one defined - // in `HTLCEntry.HtlcIndex` as previously this field was encoded - // using `uint16`, thus we will read it as raw bytes and - // deserialize it further below. - htlcIndexBlob tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob] - ) - - for { - var htlc HTLCEntry - - customBlob := htlc.CustomBlob.Zero() - htlcIndex := htlcIndexBlob.Zero() - - // Create the tlv stream. - records := []tlv.Record{ - htlc.RHash.Record(), - htlc.RefundTimeout.Record(), - htlc.OutputIndex.Record(), - htlc.Incoming.Record(), - htlc.Amt.Record(), - customBlob.Record(), - htlcIndex.Record(), - } - - tlvStream, err := tlv.NewStream(records...) - if err != nil { - return nil, err - } - - // Read the HTLC entry. - parsedTypes, err := ReadTlvStream(r, tlvStream) - if err != nil { - // We've reached the end when hitting an EOF. - if errors.Is(err, io.ErrUnexpectedEOF) { - break - } - - return nil, err - } - - if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { - htlc.CustomBlob = tlv.SomeRecordT(customBlob) - } - - if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil { - record, err := deserializeHtlcIndexCompatible( - htlcIndex.Val, - ) - if err != nil { - return nil, err - } - - htlc.HtlcIndex = record - } - - // Append the entry. - htlcs = append(htlcs, &htlc) - } - - return htlcs, nil -} - -// deserializeHtlcIndexCompatible takes raw bytes and decodes it into an -// optional record that's assigned to the entry's HtlcIndex. -// -// NOTE: previously this `HtlcIndex` was a tlv record that used `uint16` to -// encode its value. Given now its value is encoded using BigSizeT, and for any -// BigSizeT, its possible length values are 1, 3, 5, and 8. This means if the -// tlv record has a length of 2, we know for sure it must be an old record -// whose value was encoded using uint16. -func deserializeHtlcIndexCompatible(rawBytes []byte) ( - tlv.OptionalRecordT[tlv.TlvType6, tlv.BigSizeT[uint64]], error) { - - var ( - // record defines the record that's used by the HtlcIndex in the - // entry. - record tlv.OptionalRecordT[ - tlv.TlvType6, tlv.BigSizeT[uint64], - ] - - // htlcIndexVal is the decoded uint64 value. - htlcIndexVal uint64 - ) - - // If the length of the tlv record is 2, it must be encoded using uint16 - // as the BigSizeT encoding cannot have this length. - if len(rawBytes) == 2 { - // Decode the raw bytes into uint16 and convert it into uint64. - htlcIndexVal = uint64(binary.BigEndian.Uint16(rawBytes)) - } else { - // This value is encoded using BigSizeT, we now use the decoder - // to deserialize the raw bytes. - r := bytes.NewBuffer(rawBytes) - - // Create a buffer to be used in the decoding process. - buf := [8]byte{} - - // Use the BigSizeT's decoder. - err := tlv.DBigSize(r, &htlcIndexVal, &buf, 8) - if err != nil { - return record, err - } - } - - record = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( - tlv.NewBigSizeT(htlcIndexVal), - )) - - return record, nil -} - -// WriteTlvStream is a helper function that encodes the tlv stream into the -// writer. -func WriteTlvStream(w io.Writer, s *tlv.Stream) error { - var b bytes.Buffer - if err := s.Encode(&b); err != nil { - return err - } - - // Write the stream's length as a varint. - err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{}) - if err != nil { - return err - } - - if _, err = w.Write(b.Bytes()); err != nil { - return err - } - - return nil -} - -// ReadTlvStream is a helper function that decodes the tlv stream from the -// reader. -func ReadTlvStream(r io.Reader, s *tlv.Stream) (tlv.TypeMap, error) { - var bodyLen uint64 - - // Read the stream's length. - bodyLen, err := tlv.ReadVarInt(r, &[8]byte{}) - switch { - // We'll convert any EOFs to ErrUnexpectedEOF, since this results in an - // invalid record. - case errors.Is(err, io.EOF): - return nil, io.ErrUnexpectedEOF - - // Other unexpected errors. - case err != nil: - return nil, err - } - - // TODO(yy): add overflow check. - lr := io.LimitReader(r, int64(bodyLen)) - - return s.DecodeWithParsedTypes(lr) +// revocationLogKey converts a uint64 into an 8 byte revocation log key. +func revocationLogKey(updateNum uint64) [8]byte { + var key [8]byte + byteOrder.PutUint64(key[:], updateNum) + return key } diff --git a/chanstate/kv_serialization.go b/chanstate/kv_serialization.go deleted file mode 100644 index 464e18d227..0000000000 --- a/chanstate/kv_serialization.go +++ /dev/null @@ -1,67 +0,0 @@ -package chanstate - -import ( - "encoding/binary" - "io" -) - -var byteOrder = binary.BigEndian - -// serializeLogUpdate writes a log update to the provided io.Writer. -func serializeLogUpdate(w io.Writer, l *LogUpdate) error { - return WriteElements(w, l.LogIndex, l.UpdateMsg) -} - -// deserializeLogUpdate reads a log update from the provided io.Reader. -func deserializeLogUpdate(r io.Reader) (*LogUpdate, error) { - l := &LogUpdate{} - if err := ReadElements(r, &l.LogIndex, &l.UpdateMsg); err != nil { - return nil, err - } - - return l, nil -} - -// SerializeLogUpdates serializes provided list of updates to a stream. -func SerializeLogUpdates(w io.Writer, logUpdates []LogUpdate) error { - numUpdates := uint16(len(logUpdates)) - if err := binary.Write(w, byteOrder, numUpdates); err != nil { - return err - } - - for _, diff := range logUpdates { - err := WriteElements(w, diff.LogIndex, diff.UpdateMsg) - if err != nil { - return err - } - } - - return nil -} - -// DeserializeLogUpdates deserializes a list of updates from a stream. -func DeserializeLogUpdates(r io.Reader) ([]LogUpdate, error) { - var numUpdates uint16 - if err := binary.Read(r, byteOrder, &numUpdates); err != nil { - return nil, err - } - - logUpdates := make([]LogUpdate, numUpdates) - for i := 0; i < int(numUpdates); i++ { - err := ReadElements(r, - &logUpdates[i].LogIndex, &logUpdates[i].UpdateMsg, - ) - if err != nil { - return nil, err - } - } - - return logUpdates, nil -} - -// makeLogKey converts a uint64 into an 8 byte array. -func makeLogKey(updateNum uint64) [8]byte { - var key [8]byte - byteOrder.PutUint64(key[:], updateNum) - return key -} diff --git a/chanstate/revocation_log.go b/chanstate/revocation_log.go index 0f1635e348..7de10fbbe6 100644 --- a/chanstate/revocation_log.go +++ b/chanstate/revocation_log.go @@ -2,6 +2,8 @@ package chanstate import ( "bytes" + "encoding/binary" + "errors" "io" "math" @@ -140,6 +142,31 @@ type HTLCEntry struct { HtlcIndex tlv.OptionalRecordT[tlv.TlvType6, tlv.BigSizeT[uint64]] } +// toTlvStream converts an HTLCEntry record into a tlv representation. +func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { + records := []tlv.Record{ + h.RHash.Record(), + h.RefundTimeout.Record(), + h.OutputIndex.Record(), + h.Incoming.Record(), + h.Amt.Record(), + } + + h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + + h.HtlcIndex.WhenSome(func(r tlv.RecordT[tlv.TlvType6, + tlv.BigSizeT[uint64]]) { + + records = append(records, r.Record()) + }) + + tlv.SortRecords(records) + + return tlv.NewStream(records...) +} + // NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC. func NewHTLCEntryFromHTLC(htlc HTLC) (*HTLCEntry, error) { h := &HTLCEntry{ @@ -260,3 +287,272 @@ func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, return rl } + +// SerializeRevocationLog serializes a RevocationLog record based on tlv +// format. +func SerializeRevocationLog(w io.Writer, rl *RevocationLog) error { + // Add the tlv records for all non-optional fields. + records := []tlv.Record{ + rl.OurOutputIndex.Record(), + rl.TheirOutputIndex.Record(), + rl.CommitTxHash.Record(), + } + + // Now we add any optional fields that are non-nil. + rl.OurBalance.WhenSome( + func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) { + records = append(records, r.Record()) + }, + ) + + rl.TheirBalance.WhenSome( + func(r tlv.RecordT[tlv.TlvType4, BigSizeMilliSatoshi]) { + records = append(records, r.Record()) + }, + ) + + rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + // Write the tlv stream. + if err := WriteTlvStream(w, tlvStream); err != nil { + return err + } + + // Write the HTLCs. + return SerializeHTLCEntries(w, rl.HTLCEntries) +} + +// SerializeHTLCEntries serializes a list of HTLCEntry records based on tlv +// format. +func SerializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { + for _, htlc := range htlcs { + // Create the tlv stream. + tlvStream, err := htlc.toTlvStream() + if err != nil { + return err + } + + // Write the tlv stream. + if err := WriteTlvStream(w, tlvStream); err != nil { + return err + } + } + + return nil +} + +// DeserializeRevocationLog deserializes a RevocationLog based on tlv format. +func DeserializeRevocationLog(r io.Reader) (RevocationLog, error) { + var rl RevocationLog + + ourBalance := rl.OurBalance.Zero() + theirBalance := rl.TheirBalance.Zero() + customBlob := rl.CustomBlob.Zero() + + // Create the tlv stream. + tlvStream, err := tlv.NewStream( + rl.OurOutputIndex.Record(), + rl.TheirOutputIndex.Record(), + rl.CommitTxHash.Record(), + ourBalance.Record(), + theirBalance.Record(), + customBlob.Record(), + ) + if err != nil { + return rl, err + } + + // Read the tlv stream. + parsedTypes, err := ReadTlvStream(r, tlvStream) + if err != nil { + return rl, err + } + + if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil { + rl.OurBalance = tlv.SomeRecordT(ourBalance) + } + + if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil { + rl.TheirBalance = tlv.SomeRecordT(theirBalance) + } + + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + rl.CustomBlob = tlv.SomeRecordT(customBlob) + } + + // Read the HTLC entries. + rl.HTLCEntries, err = DeserializeHTLCEntries(r) + + return rl, err +} + +// DeserializeHTLCEntries deserializes a list of HTLC entries based on tlv +// format. +func DeserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { + var ( + htlcs []*HTLCEntry + + // htlcIndexBlob defines the tlv record type to be used when + // decoding from the disk. We use it instead of the one defined + // in `HTLCEntry.HtlcIndex` as previously this field was encoded + // using `uint16`, thus we will read it as raw bytes and + // deserialize it further below. + htlcIndexBlob tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob] + ) + + for { + var htlc HTLCEntry + + customBlob := htlc.CustomBlob.Zero() + htlcIndex := htlcIndexBlob.Zero() + + // Create the tlv stream. + records := []tlv.Record{ + htlc.RHash.Record(), + htlc.RefundTimeout.Record(), + htlc.OutputIndex.Record(), + htlc.Incoming.Record(), + htlc.Amt.Record(), + customBlob.Record(), + htlcIndex.Record(), + } + + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return nil, err + } + + // Read the HTLC entry. + parsedTypes, err := ReadTlvStream(r, tlvStream) + if err != nil { + // We've reached the end when hitting an EOF. + if errors.Is(err, io.ErrUnexpectedEOF) { + break + } + + return nil, err + } + + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + htlc.CustomBlob = tlv.SomeRecordT(customBlob) + } + + if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil { + record, err := deserializeHtlcIndexCompatible( + htlcIndex.Val, + ) + if err != nil { + return nil, err + } + + htlc.HtlcIndex = record + } + + // Append the entry. + htlcs = append(htlcs, &htlc) + } + + return htlcs, nil +} + +// deserializeHtlcIndexCompatible takes raw bytes and decodes it into an +// optional record that's assigned to the entry's HtlcIndex. +// +// NOTE: previously this `HtlcIndex` was a tlv record that used `uint16` to +// encode its value. Given now its value is encoded using BigSizeT, and for any +// BigSizeT, its possible length values are 1, 3, 5, and 8. This means if the +// tlv record has a length of 2, we know for sure it must be an old record +// whose value was encoded using uint16. +func deserializeHtlcIndexCompatible(rawBytes []byte) ( + tlv.OptionalRecordT[tlv.TlvType6, tlv.BigSizeT[uint64]], error) { + + var ( + // record defines the record that's used by the HtlcIndex in the + // entry. + record tlv.OptionalRecordT[ + tlv.TlvType6, tlv.BigSizeT[uint64], + ] + + // htlcIndexVal is the decoded uint64 value. + htlcIndexVal uint64 + ) + + // If the length of the tlv record is 2, it must be encoded using uint16 + // as the BigSizeT encoding cannot have this length. + if len(rawBytes) == 2 { + // Decode the raw bytes into uint16 and convert it into uint64. + htlcIndexVal = uint64(binary.BigEndian.Uint16(rawBytes)) + } else { + // This value is encoded using BigSizeT, we now use the decoder + // to deserialize the raw bytes. + r := bytes.NewBuffer(rawBytes) + + // Create a buffer to be used in the decoding process. + buf := [8]byte{} + + // Use the BigSizeT's decoder. + err := tlv.DBigSize(r, &htlcIndexVal, &buf, 8) + if err != nil { + return record, err + } + } + + record = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( + tlv.NewBigSizeT(htlcIndexVal), + )) + + return record, nil +} + +// WriteTlvStream is a helper function that encodes the tlv stream into the +// writer. +func WriteTlvStream(w io.Writer, s *tlv.Stream) error { + var b bytes.Buffer + if err := s.Encode(&b); err != nil { + return err + } + + // Write the stream's length as a varint. + err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{}) + if err != nil { + return err + } + + if _, err = w.Write(b.Bytes()); err != nil { + return err + } + + return nil +} + +// ReadTlvStream is a helper function that decodes the tlv stream from the +// reader. +func ReadTlvStream(r io.Reader, s *tlv.Stream) (tlv.TypeMap, error) { + var bodyLen uint64 + + // Read the stream's length. + bodyLen, err := tlv.ReadVarInt(r, &[8]byte{}) + switch { + // We'll convert any EOFs to ErrUnexpectedEOF, since this results in an + // invalid record. + case errors.Is(err, io.EOF): + return nil, io.ErrUnexpectedEOF + + // Other unexpected errors. + case err != nil: + return nil, err + } + + // TODO(yy): add overflow check. + lr := io.LimitReader(r, int64(bodyLen)) + + return s.DecodeWithParsedTypes(lr) +} From 95d228a8b0636e41706869565e6886f374a4f46a Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 00:09:20 -0300 Subject: [PATCH 23/61] chanstate: group open channel kv tlv data Rename the open channel TLV data helper into the KV open channel domain and fold in the key locator TLV adapter. This keeps the open channel storage records and their encode/decode helpers in one KV store file without changing the exported helper API. --- chanstate/key_locator.go | 57 ------ chanstate/kv_open_channel.go | 270 +++++++++++++++++++++++++++++ chanstate/open_channel_tlv_data.go | 228 ------------------------ 3 files changed, 270 insertions(+), 285 deletions(-) delete mode 100644 chanstate/key_locator.go delete mode 100644 chanstate/open_channel_tlv_data.go diff --git a/chanstate/key_locator.go b/chanstate/key_locator.go deleted file mode 100644 index 8ca357802d..0000000000 --- a/chanstate/key_locator.go +++ /dev/null @@ -1,57 +0,0 @@ -package chanstate - -import ( - "io" - - "github.com/lightningnetwork/lnd/keychain" - "github.com/lightningnetwork/lnd/tlv" -) - -// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the -// tlv.RecordProducer interface. -type keyLocRecord struct { - keychain.KeyLocator -} - -// Record creates a Record out of a KeyLocator using the passed Type and the -// EKeyLocator and DKeyLocator functions. The size will always be 8 as -// KeyFamily is uint32 and the Index is uint32. -// -// NOTE: This is part of the tlv.RecordProducer interface. -func (k *keyLocRecord) Record() tlv.Record { - // Note that we set the type here as zero, as when used with a - // tlv.RecordT, the type param will be used as the type. - return tlv.MakeStaticRecord( - 0, &k.KeyLocator, 8, EKeyLocator, DKeyLocator, - ) -} - -// EKeyLocator is an encoder for keychain.KeyLocator. -func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { - if v, ok := val.(*keychain.KeyLocator); ok { - err := tlv.EUint32T(w, uint32(v.Family), buf) - if err != nil { - return err - } - - return tlv.EUint32T(w, v.Index, buf) - } - - return tlv.NewTypeForEncodingErr(val, "keychain.KeyLocator") -} - -// DKeyLocator is a decoder for keychain.KeyLocator. -func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { - if v, ok := val.(*keychain.KeyLocator); ok { - var family uint32 - err := tlv.DUint32(r, &family, buf, 4) - if err != nil { - return err - } - v.Family = keychain.KeyFamily(family) - - return tlv.DUint32(r, &v.Index, buf, 4) - } - - return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) -} diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index a440f494a4..e92d24497e 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -8,8 +8,11 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/wire/v2" + "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" ) @@ -278,3 +281,270 @@ func FetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey, return chanBucket, nil } + +// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the +// tlv.RecordProducer interface. +type keyLocRecord struct { + keychain.KeyLocator +} + +// Record creates a Record out of a KeyLocator using the passed Type and the +// EKeyLocator and DKeyLocator functions. The size will always be 8 as +// KeyFamily is uint32 and the Index is uint32. +// +// NOTE: This is part of the tlv.RecordProducer interface. +func (k *keyLocRecord) Record() tlv.Record { + // Note that we set the type here as zero, as when used with a + // tlv.RecordT, the type param will be used as the type. + return tlv.MakeStaticRecord( + 0, &k.KeyLocator, 8, EKeyLocator, DKeyLocator, + ) +} + +// EKeyLocator is an encoder for keychain.KeyLocator. +func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*keychain.KeyLocator); ok { + err := tlv.EUint32T(w, uint32(v.Family), buf) + if err != nil { + return err + } + + return tlv.EUint32T(w, v.Index, buf) + } + + return tlv.NewTypeForEncodingErr(val, "keychain.KeyLocator") +} + +// DKeyLocator is a decoder for keychain.KeyLocator. +func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*keychain.KeyLocator); ok { + var family uint32 + err := tlv.DUint32(r, &family, buf, 4) + if err != nil { + return err + } + v.Family = keychain.KeyFamily(family) + + return tlv.DUint32(r, &v.Index, buf, 4) + } + + return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) +} + +// openChannelTlvData houses the new data fields that are stored for each +// channel in a TLV stream within the root bucket. This is stored as a TLV +// stream appended to the existing hard-coded fields in the channel's root +// bucket. New fields being added to the channel state should be added here. +// +// NOTE: This struct is used for serialization purposes only and its fields +// should be accessed via the OpenChannel struct while in memory. +type openChannelTlvData struct { + // revokeKeyLoc is the key locator for the revocation key. + revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord] + + // initialLocalBalance is the initial local balance of the channel. + initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64] + + // initialRemoteBalance is the initial remote balance of the channel. + initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64] + + // realScid is the real short channel ID of the channel corresponding to + // the on-chain outpoint. + realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID] + + // memo is an optional text field that gives context to the user about + // the channel. + memo tlv.OptionalRecordT[tlv.TlvType5, []byte] + + // tapscriptRoot is the optional Tapscript root the channel funding + // output commits to. + tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] + + // customBlob is an optional TLV encoded blob of data representing + // custom channel funding information. + customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob] + + // confirmationHeight records the block height at which the funding + // transaction was first confirmed. + confirmationHeight tlv.RecordT[tlv.TlvType8, uint32] + + // closeConfirmationHeight records the block height at which the closing + // transaction was first confirmed. This is used to calculate the + // remaining confirmations until the channel is considered fully closed. + // Note: if not set, it means either the channel has not been + // closed yet, or it was closed before this field was introduced. + closeConfirmationHeight tlv.OptionalRecordT[tlv.TlvType9, uint32] +} + +// encode serializes the openChannelTlvData to the given io.Writer. +func (c *openChannelTlvData) encode(w io.Writer) error { + tlvRecords := []tlv.Record{ + c.revokeKeyLoc.Record(), + c.initialLocalBalance.Record(), + c.initialRemoteBalance.Record(), + c.realScid.Record(), + c.confirmationHeight.Record(), + } + c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { + tlvRecords = append(tlvRecords, memo.Record()) + }) + c.tapscriptRoot.WhenSome( + func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { + tlvRecords = append(tlvRecords, root.Record()) + }, + ) + c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) { + tlvRecords = append(tlvRecords, blob.Record()) + }) + c.closeConfirmationHeight.WhenSome( + func(h tlv.RecordT[tlv.TlvType9, uint32]) { + tlvRecords = append(tlvRecords, h.Record()) + }, + ) + + tlv.SortRecords(tlvRecords) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// decode deserializes the openChannelTlvData from the given io.Reader. +func (c *openChannelTlvData) decode(r io.Reader) error { + memo := c.memo.Zero() + tapscriptRoot := c.tapscriptRoot.Zero() + blob := c.customBlob.Zero() + closeConfHeight := c.closeConfirmationHeight.Zero() + + // Create the tlv stream. + tlvStream, err := tlv.NewStream( + c.revokeKeyLoc.Record(), + c.initialLocalBalance.Record(), + c.initialRemoteBalance.Record(), + c.realScid.Record(), + memo.Record(), + tapscriptRoot.Record(), + blob.Record(), + c.confirmationHeight.Record(), + closeConfHeight.Record(), + ) + if err != nil { + return err + } + + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[memo.TlvType()]; ok { + c.memo = tlv.SomeRecordT(memo) + } + if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { + c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) + } + if _, ok := tlvs[c.customBlob.TlvType()]; ok { + c.customBlob = tlv.SomeRecordT(blob) + } + if _, ok := tlvs[closeConfHeight.TlvType()]; ok { + c.closeConfirmationHeight = tlv.SomeRecordT(closeConfHeight) + } + + return nil +} + +// DecodeOpenChannelTlvData decodes and applies auxiliary TLV data to an open +// channel. +func DecodeOpenChannelTlvData(r io.Reader, channel *OpenChannel) error { + var auxData openChannelTlvData + if err := auxData.decode(r); err != nil { + return err + } + + amendOpenChannelTlvData(channel, auxData) + + return nil +} + +// EncodeOpenChannelTlvData extracts and encodes auxiliary TLV data from an open +// channel. +func EncodeOpenChannelTlvData(w io.Writer, channel *OpenChannel) error { + auxData := extractOpenChannelTlvData(channel) + return auxData.encode(w) +} + +// amendOpenChannelTlvData updates the channel with the given auxiliary TLV +// data. +func amendOpenChannelTlvData(channel *OpenChannel, auxData openChannelTlvData) { + channel.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator + channel.InitialLocalBalance = lnwire.MilliSatoshi( + auxData.initialLocalBalance.Val, + ) + channel.InitialRemoteBalance = lnwire.MilliSatoshi( + auxData.initialRemoteBalance.Val, + ) + channel.SetConfirmedScidForStore(auxData.realScid.Val) + channel.ConfirmationHeight = auxData.confirmationHeight.Val + + auxData.memo.WhenSomeV(func(memo []byte) { + channel.Memo = memo + }) + auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) { + channel.TapscriptRoot = fn.Some[chainhash.Hash](h) + }) + auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { + channel.CustomBlob = fn.Some(blob) + }) + auxData.closeConfirmationHeight.WhenSomeV(func(h uint32) { + channel.CloseConfirmationHeight = fn.Some(h) + }) +} + +// extractOpenChannelTlvData creates a new openChannelTlvData from the given +// channel. +func extractOpenChannelTlvData(channel *OpenChannel) openChannelTlvData { + auxData := openChannelTlvData{ + revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1]( + keyLocRecord{channel.RevocationKeyLocator}, + ), + initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2]( + uint64(channel.InitialLocalBalance), + ), + initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3]( + uint64(channel.InitialRemoteBalance), + ), + realScid: tlv.NewRecordT[tlv.TlvType4]( + channel.ConfirmedScidForStore(), + ), + confirmationHeight: tlv.NewPrimitiveRecord[tlv.TlvType8]( + channel.ConfirmationHeight, + ), + } + + if len(channel.Memo) != 0 { + auxData.memo = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5](channel.Memo), + ) + } + channel.TapscriptRoot.WhenSome(func(h chainhash.Hash) { + auxData.tapscriptRoot = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h), + ) + }) + channel.CustomBlob.WhenSome(func(blob tlv.Blob) { + auxData.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType7](blob), + ) + }) + channel.CloseConfirmationHeight.WhenSome(func(h uint32) { + auxData.closeConfirmationHeight = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType9](h), + ) + }) + + return auxData +} diff --git a/chanstate/open_channel_tlv_data.go b/chanstate/open_channel_tlv_data.go deleted file mode 100644 index 643f590332..0000000000 --- a/chanstate/open_channel_tlv_data.go +++ /dev/null @@ -1,228 +0,0 @@ -package chanstate - -import ( - "io" - - "github.com/btcsuite/btcd/chainhash/v2" - "github.com/lightningnetwork/lnd/fn/v2" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/tlv" -) - -// openChannelTlvData houses the new data fields that are stored for each -// channel in a TLV stream within the root bucket. This is stored as a TLV -// stream appended to the existing hard-coded fields in the channel's root -// bucket. New fields being added to the channel state should be added here. -// -// NOTE: This struct is used for serialization purposes only and its fields -// should be accessed via the OpenChannel struct while in memory. -type openChannelTlvData struct { - // revokeKeyLoc is the key locator for the revocation key. - revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord] - - // initialLocalBalance is the initial local balance of the channel. - initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64] - - // initialRemoteBalance is the initial remote balance of the channel. - initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64] - - // realScid is the real short channel ID of the channel corresponding to - // the on-chain outpoint. - realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID] - - // memo is an optional text field that gives context to the user about - // the channel. - memo tlv.OptionalRecordT[tlv.TlvType5, []byte] - - // tapscriptRoot is the optional Tapscript root the channel funding - // output commits to. - tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] - - // customBlob is an optional TLV encoded blob of data representing - // custom channel funding information. - customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob] - - // confirmationHeight records the block height at which the funding - // transaction was first confirmed. - confirmationHeight tlv.RecordT[tlv.TlvType8, uint32] - - // closeConfirmationHeight records the block height at which the closing - // transaction was first confirmed. This is used to calculate the - // remaining confirmations until the channel is considered fully closed. - // Note: if not set, it means either the channel has not been - // closed yet, or it was closed before this field was introduced. - closeConfirmationHeight tlv.OptionalRecordT[tlv.TlvType9, uint32] -} - -// encode serializes the openChannelTlvData to the given io.Writer. -func (c *openChannelTlvData) encode(w io.Writer) error { - tlvRecords := []tlv.Record{ - c.revokeKeyLoc.Record(), - c.initialLocalBalance.Record(), - c.initialRemoteBalance.Record(), - c.realScid.Record(), - c.confirmationHeight.Record(), - } - c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { - tlvRecords = append(tlvRecords, memo.Record()) - }) - c.tapscriptRoot.WhenSome( - func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { - tlvRecords = append(tlvRecords, root.Record()) - }, - ) - c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) { - tlvRecords = append(tlvRecords, blob.Record()) - }) - c.closeConfirmationHeight.WhenSome( - func(h tlv.RecordT[tlv.TlvType9, uint32]) { - tlvRecords = append(tlvRecords, h.Record()) - }, - ) - - tlv.SortRecords(tlvRecords) - - // Create the tlv stream. - tlvStream, err := tlv.NewStream(tlvRecords...) - if err != nil { - return err - } - - return tlvStream.Encode(w) -} - -// decode deserializes the openChannelTlvData from the given io.Reader. -func (c *openChannelTlvData) decode(r io.Reader) error { - memo := c.memo.Zero() - tapscriptRoot := c.tapscriptRoot.Zero() - blob := c.customBlob.Zero() - closeConfHeight := c.closeConfirmationHeight.Zero() - - // Create the tlv stream. - tlvStream, err := tlv.NewStream( - c.revokeKeyLoc.Record(), - c.initialLocalBalance.Record(), - c.initialRemoteBalance.Record(), - c.realScid.Record(), - memo.Record(), - tapscriptRoot.Record(), - blob.Record(), - c.confirmationHeight.Record(), - closeConfHeight.Record(), - ) - if err != nil { - return err - } - - tlvs, err := tlvStream.DecodeWithParsedTypes(r) - if err != nil { - return err - } - - if _, ok := tlvs[memo.TlvType()]; ok { - c.memo = tlv.SomeRecordT(memo) - } - if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { - c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) - } - if _, ok := tlvs[c.customBlob.TlvType()]; ok { - c.customBlob = tlv.SomeRecordT(blob) - } - if _, ok := tlvs[closeConfHeight.TlvType()]; ok { - c.closeConfirmationHeight = tlv.SomeRecordT(closeConfHeight) - } - - return nil -} - -// DecodeOpenChannelTlvData decodes and applies auxiliary TLV data to an open -// channel. -func DecodeOpenChannelTlvData(r io.Reader, channel *OpenChannel) error { - var auxData openChannelTlvData - if err := auxData.decode(r); err != nil { - return err - } - - amendOpenChannelTlvData(channel, auxData) - - return nil -} - -// EncodeOpenChannelTlvData extracts and encodes auxiliary TLV data from an open -// channel. -func EncodeOpenChannelTlvData(w io.Writer, channel *OpenChannel) error { - auxData := extractOpenChannelTlvData(channel) - return auxData.encode(w) -} - -// amendOpenChannelTlvData updates the channel with the given auxiliary TLV -// data. -func amendOpenChannelTlvData(channel *OpenChannel, auxData openChannelTlvData) { - channel.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator - channel.InitialLocalBalance = lnwire.MilliSatoshi( - auxData.initialLocalBalance.Val, - ) - channel.InitialRemoteBalance = lnwire.MilliSatoshi( - auxData.initialRemoteBalance.Val, - ) - channel.SetConfirmedScidForStore(auxData.realScid.Val) - channel.ConfirmationHeight = auxData.confirmationHeight.Val - - auxData.memo.WhenSomeV(func(memo []byte) { - channel.Memo = memo - }) - auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) { - channel.TapscriptRoot = fn.Some[chainhash.Hash](h) - }) - auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { - channel.CustomBlob = fn.Some(blob) - }) - auxData.closeConfirmationHeight.WhenSomeV(func(h uint32) { - channel.CloseConfirmationHeight = fn.Some(h) - }) -} - -// extractOpenChannelTlvData creates a new openChannelTlvData from the given -// channel. -func extractOpenChannelTlvData(channel *OpenChannel) openChannelTlvData { - auxData := openChannelTlvData{ - revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1]( - keyLocRecord{channel.RevocationKeyLocator}, - ), - initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2]( - uint64(channel.InitialLocalBalance), - ), - initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3]( - uint64(channel.InitialRemoteBalance), - ), - realScid: tlv.NewRecordT[tlv.TlvType4]( - channel.ConfirmedScidForStore(), - ), - confirmationHeight: tlv.NewPrimitiveRecord[tlv.TlvType8]( - channel.ConfirmationHeight, - ), - } - - if len(channel.Memo) != 0 { - auxData.memo = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType5](channel.Memo), - ) - } - channel.TapscriptRoot.WhenSome(func(h chainhash.Hash) { - auxData.tapscriptRoot = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h), - ) - }) - channel.CustomBlob.WhenSome(func(blob tlv.Blob) { - auxData.customBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType7](blob), - ) - }) - channel.CloseConfirmationHeight.WhenSome(func(h uint32) { - auxData.closeConfirmationHeight = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType9](h), - ) - }) - - return auxData -} From d1de61e5aa2653ce13e186e9a28142d5743a3e2c Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 00:11:44 -0300 Subject: [PATCH 24/61] chanstate: move config kv serialization Move channel config read/write helpers out of the pure config type file and into the open channel KV domain file. This keeps storage serialization colocated with the KV record helpers that use it while preserving the existing exported helper names. --- chanstate/config.go | 23 ----------------------- chanstate/kv_open_channel.go | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/chanstate/config.go b/chanstate/config.go index 398cc6e16c..e9adecf258 100644 --- a/chanstate/config.go +++ b/chanstate/config.go @@ -1,8 +1,6 @@ package chanstate import ( - "io" - "github.com/btcsuite/btcd/btcutil/v2" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" @@ -108,24 +106,3 @@ type ChannelConfig struct { // within any HTLC output scripts. HtlcBasePoint keychain.KeyDescriptor } - -// WriteChanConfig serializes a channel config. -func WriteChanConfig(b io.Writer, c *ChannelConfig) error { - return WriteElements(b, - c.DustLimit, c.MaxPendingAmount, c.ChanReserve, c.MinHTLC, - c.MaxAcceptedHtlcs, c.CsvDelay, c.MultiSigKey, - c.RevocationBasePoint, c.PaymentBasePoint, c.DelayBasePoint, - c.HtlcBasePoint, - ) -} - -// ReadChanConfig deserializes a channel config. -func ReadChanConfig(b io.Reader, c *ChannelConfig) error { - return ReadElements(b, - &c.DustLimit, &c.MaxPendingAmount, &c.ChanReserve, - &c.MinHTLC, &c.MaxAcceptedHtlcs, &c.CsvDelay, - &c.MultiSigKey, &c.RevocationBasePoint, - &c.PaymentBasePoint, &c.DelayBasePoint, - &c.HtlcBasePoint, - ) -} diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index e92d24497e..1e3ca1d487 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -331,6 +331,27 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) } +// WriteChanConfig serializes a channel config. +func WriteChanConfig(b io.Writer, c *ChannelConfig) error { + return WriteElements(b, + c.DustLimit, c.MaxPendingAmount, c.ChanReserve, c.MinHTLC, + c.MaxAcceptedHtlcs, c.CsvDelay, c.MultiSigKey, + c.RevocationBasePoint, c.PaymentBasePoint, c.DelayBasePoint, + c.HtlcBasePoint, + ) +} + +// ReadChanConfig deserializes a channel config. +func ReadChanConfig(b io.Reader, c *ChannelConfig) error { + return ReadElements(b, + &c.DustLimit, &c.MaxPendingAmount, &c.ChanReserve, + &c.MinHTLC, &c.MaxAcceptedHtlcs, &c.CsvDelay, + &c.MultiSigKey, &c.RevocationBasePoint, + &c.PaymentBasePoint, &c.DelayBasePoint, + &c.HtlcBasePoint, + ) +} + // openChannelTlvData houses the new data fields that are stored for each // channel in a TLV stream within the root bucket. This is stored as a TLV // stream appended to the existing hard-coded fields in the channel's root From a28ed3c6585de066f16456e0259d68d58dc9ef5d Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 00:15:37 -0300 Subject: [PATCH 25/61] chanstate: move revocation kv serialization Move revocation log stream serialization and deserialization helpers into the revocation KV store file. This keeps the pure revocation log types separate from the on-disk encoding helpers while preserving the existing function bodies and comments. --- chanstate/kv_revocation_log.go | 271 +++++++++++++++++++++++++++++++++ chanstate/revocation_log.go | 271 --------------------------------- 2 files changed, 271 insertions(+), 271 deletions(-) diff --git a/chanstate/kv_revocation_log.go b/chanstate/kv_revocation_log.go index bae42a3a93..4f722baa64 100644 --- a/chanstate/kv_revocation_log.go +++ b/chanstate/kv_revocation_log.go @@ -2,7 +2,9 @@ package chanstate import ( "bytes" + "encoding/binary" "errors" + "io" "math" "github.com/lightningnetwork/lnd/kvdb" @@ -121,6 +123,275 @@ func FetchRevocationLog(log kvdb.RBucket, return DeserializeRevocationLog(commitReader) } +// SerializeRevocationLog serializes a RevocationLog record based on tlv +// format. +func SerializeRevocationLog(w io.Writer, rl *RevocationLog) error { + // Add the tlv records for all non-optional fields. + records := []tlv.Record{ + rl.OurOutputIndex.Record(), + rl.TheirOutputIndex.Record(), + rl.CommitTxHash.Record(), + } + + // Now we add any optional fields that are non-nil. + rl.OurBalance.WhenSome( + func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) { + records = append(records, r.Record()) + }, + ) + + rl.TheirBalance.WhenSome( + func(r tlv.RecordT[tlv.TlvType4, BigSizeMilliSatoshi]) { + records = append(records, r.Record()) + }, + ) + + rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + // Write the tlv stream. + if err := WriteTlvStream(w, tlvStream); err != nil { + return err + } + + // Write the HTLCs. + return SerializeHTLCEntries(w, rl.HTLCEntries) +} + +// SerializeHTLCEntries serializes a list of HTLCEntry records based on tlv +// format. +func SerializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { + for _, htlc := range htlcs { + // Create the tlv stream. + tlvStream, err := htlc.toTlvStream() + if err != nil { + return err + } + + // Write the tlv stream. + if err := WriteTlvStream(w, tlvStream); err != nil { + return err + } + } + + return nil +} + +// DeserializeRevocationLog deserializes a RevocationLog based on tlv format. +func DeserializeRevocationLog(r io.Reader) (RevocationLog, error) { + var rl RevocationLog + + ourBalance := rl.OurBalance.Zero() + theirBalance := rl.TheirBalance.Zero() + customBlob := rl.CustomBlob.Zero() + + // Create the tlv stream. + tlvStream, err := tlv.NewStream( + rl.OurOutputIndex.Record(), + rl.TheirOutputIndex.Record(), + rl.CommitTxHash.Record(), + ourBalance.Record(), + theirBalance.Record(), + customBlob.Record(), + ) + if err != nil { + return rl, err + } + + // Read the tlv stream. + parsedTypes, err := ReadTlvStream(r, tlvStream) + if err != nil { + return rl, err + } + + if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil { + rl.OurBalance = tlv.SomeRecordT(ourBalance) + } + + if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil { + rl.TheirBalance = tlv.SomeRecordT(theirBalance) + } + + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + rl.CustomBlob = tlv.SomeRecordT(customBlob) + } + + // Read the HTLC entries. + rl.HTLCEntries, err = DeserializeHTLCEntries(r) + + return rl, err +} + +// DeserializeHTLCEntries deserializes a list of HTLC entries based on tlv +// format. +func DeserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { + var ( + htlcs []*HTLCEntry + + // htlcIndexBlob defines the tlv record type to be used when + // decoding from the disk. We use it instead of the one defined + // in `HTLCEntry.HtlcIndex` as previously this field was encoded + // using `uint16`, thus we will read it as raw bytes and + // deserialize it further below. + htlcIndexBlob tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob] + ) + + for { + var htlc HTLCEntry + + customBlob := htlc.CustomBlob.Zero() + htlcIndex := htlcIndexBlob.Zero() + + // Create the tlv stream. + records := []tlv.Record{ + htlc.RHash.Record(), + htlc.RefundTimeout.Record(), + htlc.OutputIndex.Record(), + htlc.Incoming.Record(), + htlc.Amt.Record(), + customBlob.Record(), + htlcIndex.Record(), + } + + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return nil, err + } + + // Read the HTLC entry. + parsedTypes, err := ReadTlvStream(r, tlvStream) + if err != nil { + // We've reached the end when hitting an EOF. + if errors.Is(err, io.ErrUnexpectedEOF) { + break + } + + return nil, err + } + + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + htlc.CustomBlob = tlv.SomeRecordT(customBlob) + } + + if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil { + record, err := deserializeHtlcIndexCompatible( + htlcIndex.Val, + ) + if err != nil { + return nil, err + } + + htlc.HtlcIndex = record + } + + // Append the entry. + htlcs = append(htlcs, &htlc) + } + + return htlcs, nil +} + +// deserializeHtlcIndexCompatible takes raw bytes and decodes it into an +// optional record that's assigned to the entry's HtlcIndex. +// +// NOTE: previously this `HtlcIndex` was a tlv record that used `uint16` to +// encode its value. Given now its value is encoded using BigSizeT, and for any +// BigSizeT, its possible length values are 1, 3, 5, and 8. This means if the +// tlv record has a length of 2, we know for sure it must be an old record +// whose value was encoded using uint16. +func deserializeHtlcIndexCompatible(rawBytes []byte) ( + tlv.OptionalRecordT[tlv.TlvType6, tlv.BigSizeT[uint64]], error) { + + var ( + // record defines the record that's used by the HtlcIndex in the + // entry. + record tlv.OptionalRecordT[ + tlv.TlvType6, tlv.BigSizeT[uint64], + ] + + // htlcIndexVal is the decoded uint64 value. + htlcIndexVal uint64 + ) + + // If the length of the tlv record is 2, it must be encoded using uint16 + // as the BigSizeT encoding cannot have this length. + if len(rawBytes) == 2 { + // Decode the raw bytes into uint16 and convert it into uint64. + htlcIndexVal = uint64(binary.BigEndian.Uint16(rawBytes)) + } else { + // This value is encoded using BigSizeT, we now use the decoder + // to deserialize the raw bytes. + r := bytes.NewBuffer(rawBytes) + + // Create a buffer to be used in the decoding process. + buf := [8]byte{} + + // Use the BigSizeT's decoder. + err := tlv.DBigSize(r, &htlcIndexVal, &buf, 8) + if err != nil { + return record, err + } + } + + record = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( + tlv.NewBigSizeT(htlcIndexVal), + )) + + return record, nil +} + +// WriteTlvStream is a helper function that encodes the tlv stream into the +// writer. +func WriteTlvStream(w io.Writer, s *tlv.Stream) error { + var b bytes.Buffer + if err := s.Encode(&b); err != nil { + return err + } + + // Write the stream's length as a varint. + err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{}) + if err != nil { + return err + } + + if _, err = w.Write(b.Bytes()); err != nil { + return err + } + + return nil +} + +// ReadTlvStream is a helper function that decodes the tlv stream from the +// reader. +func ReadTlvStream(r io.Reader, s *tlv.Stream) (tlv.TypeMap, error) { + var bodyLen uint64 + + // Read the stream's length. + bodyLen, err := tlv.ReadVarInt(r, &[8]byte{}) + switch { + // We'll convert any EOFs to ErrUnexpectedEOF, since this results in an + // invalid record. + case errors.Is(err, io.EOF): + return nil, io.ErrUnexpectedEOF + + // Other unexpected errors. + case err != nil: + return nil, err + } + + // TODO(yy): add overflow check. + lr := io.LimitReader(r, int64(bodyLen)) + + return s.DecodeWithParsedTypes(lr) +} + // revocationLogKey converts a uint64 into an 8 byte revocation log key. func revocationLogKey(updateNum uint64) [8]byte { var key [8]byte diff --git a/chanstate/revocation_log.go b/chanstate/revocation_log.go index 7de10fbbe6..3cb0943cbe 100644 --- a/chanstate/revocation_log.go +++ b/chanstate/revocation_log.go @@ -2,8 +2,6 @@ package chanstate import ( "bytes" - "encoding/binary" - "errors" "io" "math" @@ -287,272 +285,3 @@ func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, return rl } - -// SerializeRevocationLog serializes a RevocationLog record based on tlv -// format. -func SerializeRevocationLog(w io.Writer, rl *RevocationLog) error { - // Add the tlv records for all non-optional fields. - records := []tlv.Record{ - rl.OurOutputIndex.Record(), - rl.TheirOutputIndex.Record(), - rl.CommitTxHash.Record(), - } - - // Now we add any optional fields that are non-nil. - rl.OurBalance.WhenSome( - func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) { - records = append(records, r.Record()) - }, - ) - - rl.TheirBalance.WhenSome( - func(r tlv.RecordT[tlv.TlvType4, BigSizeMilliSatoshi]) { - records = append(records, r.Record()) - }, - ) - - rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { - records = append(records, r.Record()) - }) - - // Create the tlv stream. - tlvStream, err := tlv.NewStream(records...) - if err != nil { - return err - } - - // Write the tlv stream. - if err := WriteTlvStream(w, tlvStream); err != nil { - return err - } - - // Write the HTLCs. - return SerializeHTLCEntries(w, rl.HTLCEntries) -} - -// SerializeHTLCEntries serializes a list of HTLCEntry records based on tlv -// format. -func SerializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { - for _, htlc := range htlcs { - // Create the tlv stream. - tlvStream, err := htlc.toTlvStream() - if err != nil { - return err - } - - // Write the tlv stream. - if err := WriteTlvStream(w, tlvStream); err != nil { - return err - } - } - - return nil -} - -// DeserializeRevocationLog deserializes a RevocationLog based on tlv format. -func DeserializeRevocationLog(r io.Reader) (RevocationLog, error) { - var rl RevocationLog - - ourBalance := rl.OurBalance.Zero() - theirBalance := rl.TheirBalance.Zero() - customBlob := rl.CustomBlob.Zero() - - // Create the tlv stream. - tlvStream, err := tlv.NewStream( - rl.OurOutputIndex.Record(), - rl.TheirOutputIndex.Record(), - rl.CommitTxHash.Record(), - ourBalance.Record(), - theirBalance.Record(), - customBlob.Record(), - ) - if err != nil { - return rl, err - } - - // Read the tlv stream. - parsedTypes, err := ReadTlvStream(r, tlvStream) - if err != nil { - return rl, err - } - - if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil { - rl.OurBalance = tlv.SomeRecordT(ourBalance) - } - - if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil { - rl.TheirBalance = tlv.SomeRecordT(theirBalance) - } - - if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { - rl.CustomBlob = tlv.SomeRecordT(customBlob) - } - - // Read the HTLC entries. - rl.HTLCEntries, err = DeserializeHTLCEntries(r) - - return rl, err -} - -// DeserializeHTLCEntries deserializes a list of HTLC entries based on tlv -// format. -func DeserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { - var ( - htlcs []*HTLCEntry - - // htlcIndexBlob defines the tlv record type to be used when - // decoding from the disk. We use it instead of the one defined - // in `HTLCEntry.HtlcIndex` as previously this field was encoded - // using `uint16`, thus we will read it as raw bytes and - // deserialize it further below. - htlcIndexBlob tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob] - ) - - for { - var htlc HTLCEntry - - customBlob := htlc.CustomBlob.Zero() - htlcIndex := htlcIndexBlob.Zero() - - // Create the tlv stream. - records := []tlv.Record{ - htlc.RHash.Record(), - htlc.RefundTimeout.Record(), - htlc.OutputIndex.Record(), - htlc.Incoming.Record(), - htlc.Amt.Record(), - customBlob.Record(), - htlcIndex.Record(), - } - - tlvStream, err := tlv.NewStream(records...) - if err != nil { - return nil, err - } - - // Read the HTLC entry. - parsedTypes, err := ReadTlvStream(r, tlvStream) - if err != nil { - // We've reached the end when hitting an EOF. - if errors.Is(err, io.ErrUnexpectedEOF) { - break - } - - return nil, err - } - - if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { - htlc.CustomBlob = tlv.SomeRecordT(customBlob) - } - - if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil { - record, err := deserializeHtlcIndexCompatible( - htlcIndex.Val, - ) - if err != nil { - return nil, err - } - - htlc.HtlcIndex = record - } - - // Append the entry. - htlcs = append(htlcs, &htlc) - } - - return htlcs, nil -} - -// deserializeHtlcIndexCompatible takes raw bytes and decodes it into an -// optional record that's assigned to the entry's HtlcIndex. -// -// NOTE: previously this `HtlcIndex` was a tlv record that used `uint16` to -// encode its value. Given now its value is encoded using BigSizeT, and for any -// BigSizeT, its possible length values are 1, 3, 5, and 8. This means if the -// tlv record has a length of 2, we know for sure it must be an old record -// whose value was encoded using uint16. -func deserializeHtlcIndexCompatible(rawBytes []byte) ( - tlv.OptionalRecordT[tlv.TlvType6, tlv.BigSizeT[uint64]], error) { - - var ( - // record defines the record that's used by the HtlcIndex in the - // entry. - record tlv.OptionalRecordT[ - tlv.TlvType6, tlv.BigSizeT[uint64], - ] - - // htlcIndexVal is the decoded uint64 value. - htlcIndexVal uint64 - ) - - // If the length of the tlv record is 2, it must be encoded using uint16 - // as the BigSizeT encoding cannot have this length. - if len(rawBytes) == 2 { - // Decode the raw bytes into uint16 and convert it into uint64. - htlcIndexVal = uint64(binary.BigEndian.Uint16(rawBytes)) - } else { - // This value is encoded using BigSizeT, we now use the decoder - // to deserialize the raw bytes. - r := bytes.NewBuffer(rawBytes) - - // Create a buffer to be used in the decoding process. - buf := [8]byte{} - - // Use the BigSizeT's decoder. - err := tlv.DBigSize(r, &htlcIndexVal, &buf, 8) - if err != nil { - return record, err - } - } - - record = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( - tlv.NewBigSizeT(htlcIndexVal), - )) - - return record, nil -} - -// WriteTlvStream is a helper function that encodes the tlv stream into the -// writer. -func WriteTlvStream(w io.Writer, s *tlv.Stream) error { - var b bytes.Buffer - if err := s.Encode(&b); err != nil { - return err - } - - // Write the stream's length as a varint. - err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{}) - if err != nil { - return err - } - - if _, err = w.Write(b.Bytes()); err != nil { - return err - } - - return nil -} - -// ReadTlvStream is a helper function that decodes the tlv stream from the -// reader. -func ReadTlvStream(r io.Reader, s *tlv.Stream) (tlv.TypeMap, error) { - var bodyLen uint64 - - // Read the stream's length. - bodyLen, err := tlv.ReadVarInt(r, &[8]byte{}) - switch { - // We'll convert any EOFs to ErrUnexpectedEOF, since this results in an - // invalid record. - case errors.Is(err, io.EOF): - return nil, io.ErrUnexpectedEOF - - // Other unexpected errors. - case err != nil: - return nil, err - } - - // TODO(yy): add overflow check. - lr := io.LimitReader(r, int64(bodyLen)) - - return s.DecodeWithParsedTypes(lr) -} From 04a10ad9b3094e11d3f930d5de98b5801977bc73 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 00:24:07 -0300 Subject: [PATCH 26/61] chanstate: move forwarding kv codecs Move AddRef and PkgFilter persistence codecs from the forwarding domain file into the forwarding KV store file. The forwarding domain types keep their pure behavior while the KV-specific byte encoding stays next to the bucket code that stores those values. --- chanstate/forwarding.go | 49 ------------------------------ chanstate/kv_forwarding_package.go | 49 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/chanstate/forwarding.go b/chanstate/forwarding.go index 49728fed2d..dc101ef76b 100644 --- a/chanstate/forwarding.go +++ b/chanstate/forwarding.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/binary" "fmt" - "io" "github.com/lightningnetwork/lnd/lnwire" ) @@ -21,24 +20,6 @@ type AddRef struct { Index uint16 } -// Encode serializes the AddRef to the given io.Writer. -func (a *AddRef) Encode(w io.Writer) error { - if err := binary.Write(w, binary.BigEndian, a.Height); err != nil { - return err - } - - return binary.Write(w, binary.BigEndian, a.Index) -} - -// Decode deserializes the AddRef from the given io.Reader. -func (a *AddRef) Decode(r io.Reader) error { - if err := binary.Read(r, binary.BigEndian, &a.Height); err != nil { - return err - } - - return binary.Read(r, binary.BigEndian, &a.Index) -} - // SettleFailRef is used to locate a Settle/Fail in another channel's FwdPkg. A // channel does not remove its own Settle/Fail htlcs, so the source is provided // to locate a db bucket belonging to another channel. @@ -160,36 +141,6 @@ func (f *PkgFilter) IsFull() bool { return true } -// Size returns number of bytes produced when the PkgFilter is serialized. -func (f *PkgFilter) Size() uint16 { - // 2 bytes for uint16 `count`, then round up number of bytes required to - // represent `count` bits. - return 2 + (f.count+7)/8 -} - -// Encode writes the filter to the provided io.Writer. -func (f *PkgFilter) Encode(w io.Writer) error { - if err := binary.Write(w, binary.BigEndian, f.count); err != nil { - return err - } - - _, err := w.Write(f.filter) - - return err -} - -// Decode reads the filter from the provided io.Reader. -func (f *PkgFilter) Decode(r io.Reader) error { - if err := binary.Read(r, binary.BigEndian, &f.count); err != nil { - return err - } - - f.filter = make([]byte, f.Size()-2) - _, err := io.ReadFull(r, f.filter) - - return err -} - // String returns a human-readable string. func (f *PkgFilter) String() string { return fmt.Sprintf("count=%v, filter=%v", f.count, f.filter) diff --git a/chanstate/kv_forwarding_package.go b/chanstate/kv_forwarding_package.go index f6e94f180b..320e1a2167 100644 --- a/chanstate/kv_forwarding_package.go +++ b/chanstate/kv_forwarding_package.go @@ -2,6 +2,7 @@ package chanstate import ( "bytes" + "encoding/binary" "errors" "io" @@ -84,6 +85,54 @@ func FwdPackagesBucketKey() []byte { return fwdPackagesKey } +// Encode serializes the AddRef to the given io.Writer. +func (a *AddRef) Encode(w io.Writer) error { + if err := binary.Write(w, binary.BigEndian, a.Height); err != nil { + return err + } + + return binary.Write(w, binary.BigEndian, a.Index) +} + +// Decode deserializes the AddRef from the given io.Reader. +func (a *AddRef) Decode(r io.Reader) error { + if err := binary.Read(r, binary.BigEndian, &a.Height); err != nil { + return err + } + + return binary.Read(r, binary.BigEndian, &a.Index) +} + +// Size returns number of bytes produced when the PkgFilter is serialized. +func (f *PkgFilter) Size() uint16 { + // 2 bytes for uint16 `count`, then round up number of bytes required to + // represent `count` bits. + return 2 + (f.count+7)/8 +} + +// Encode writes the filter to the provided io.Writer. +func (f *PkgFilter) Encode(w io.Writer) error { + if err := binary.Write(w, binary.BigEndian, f.count); err != nil { + return err + } + + _, err := w.Write(f.filter) + + return err +} + +// Decode reads the filter from the provided io.Reader. +func (f *PkgFilter) Decode(r io.Reader) error { + if err := binary.Read(r, binary.BigEndian, &f.count); err != nil { + return err + } + + f.filter = make([]byte, f.Size()-2) + _, err := io.ReadFull(r, f.filter) + + return err +} + // SettleFailAcker is a generic interface providing the ability to acknowledge // settle/fail HTLCs stored in forwarding packages. type SettleFailAcker interface { From 077f392dbd02af7f073a9506f8a41d6ca489e31d Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 00:29:00 -0300 Subject: [PATCH 27/61] chanstate: move revocation tlv adapters Move the SparsePayHash TLV adapter and HTLCEntry stream builder into the revocation KV store file. These helpers describe the revocation log on-disk TLV layout, so keep them with the rest of the KV serialization while the domain file keeps the exported types and constructors. --- chanstate/kv_revocation_log.go | 85 +++++++++++++++++++++++++++++++++ chanstate/revocation_log.go | 87 ---------------------------------- 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/chanstate/kv_revocation_log.go b/chanstate/kv_revocation_log.go index 4f722baa64..eb532d527f 100644 --- a/chanstate/kv_revocation_log.go +++ b/chanstate/kv_revocation_log.go @@ -8,6 +8,7 @@ import ( "math" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/tlv" ) @@ -123,6 +124,90 @@ func FetchRevocationLog(log kvdb.RBucket, return DeserializeRevocationLog(commitReader) } +// Record returns a tlv record for the SparsePayHash. +func (s *SparsePayHash) Record() tlv.Record { + // We use a zero for the type here, as this'll be used along with the + // RecordT type. + return tlv.MakeDynamicRecord( + 0, s, s.hashLen, + sparseHashEncoder, sparseHashDecoder, + ) +} + +// hashLen is used by MakeDynamicRecord to return the size of the RHash. +// +// NOTE: for zero hash, we return a length 0. +func (s *SparsePayHash) hashLen() uint64 { + if bytes.Equal(s[:], lntypes.ZeroHash[:]) { + return 0 + } + + return 32 +} + +// sparseHashEncoder is the customized encoder which skips encoding the empty +// hash. +func sparseHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + v, ok := val.(*SparsePayHash) + if !ok { + return tlv.NewTypeForEncodingErr(val, "SparsePayHash") + } + + // If the value is an empty hash, we will skip encoding it. + if bytes.Equal(v[:], lntypes.ZeroHash[:]) { + return nil + } + + vArray := (*[32]byte)(v) + + return tlv.EBytes32(w, vArray, buf) +} + +// sparseHashDecoder is the customized decoder which skips decoding the empty +// hash. +func sparseHashDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + v, ok := val.(*SparsePayHash) + if !ok { + return tlv.NewTypeForEncodingErr(val, "SparsePayHash") + } + + // If the length is zero, we will skip encoding the empty hash. + if l == 0 { + return nil + } + + vArray := (*[32]byte)(v) + + return tlv.DBytes32(r, vArray, buf, 32) +} + +// toTlvStream converts an HTLCEntry record into a tlv representation. +func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { + records := []tlv.Record{ + h.RHash.Record(), + h.RefundTimeout.Record(), + h.OutputIndex.Record(), + h.Incoming.Record(), + h.Amt.Record(), + } + + h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + + h.HtlcIndex.WhenSome(func(r tlv.RecordT[tlv.TlvType6, + tlv.BigSizeT[uint64]]) { + + records = append(records, r.Record()) + }) + + tlv.SortRecords(records) + + return tlv.NewStream(records...) +} + // SerializeRevocationLog serializes a RevocationLog record based on tlv // format. func SerializeRevocationLog(w io.Writer, rl *RevocationLog) error { diff --git a/chanstate/revocation_log.go b/chanstate/revocation_log.go index 3cb0943cbe..76827436ce 100644 --- a/chanstate/revocation_log.go +++ b/chanstate/revocation_log.go @@ -1,13 +1,10 @@ package chanstate import ( - "bytes" - "io" "math" "github.com/btcsuite/btcd/btcutil/v2" "github.com/lightningnetwork/lnd/fn/v2" - "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" ) @@ -35,65 +32,6 @@ func NewSparsePayHash(rHash [32]byte) SparsePayHash { return SparsePayHash(rHash) } -// Record returns a tlv record for the SparsePayHash. -func (s *SparsePayHash) Record() tlv.Record { - // We use a zero for the type here, as this'll be used along with the - // RecordT type. - return tlv.MakeDynamicRecord( - 0, s, s.hashLen, - sparseHashEncoder, sparseHashDecoder, - ) -} - -// hashLen is used by MakeDynamicRecord to return the size of the RHash. -// -// NOTE: for zero hash, we return a length 0. -func (s *SparsePayHash) hashLen() uint64 { - if bytes.Equal(s[:], lntypes.ZeroHash[:]) { - return 0 - } - - return 32 -} - -// sparseHashEncoder is the customized encoder which skips encoding the empty -// hash. -func sparseHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error { - v, ok := val.(*SparsePayHash) - if !ok { - return tlv.NewTypeForEncodingErr(val, "SparsePayHash") - } - - // If the value is an empty hash, we will skip encoding it. - if bytes.Equal(v[:], lntypes.ZeroHash[:]) { - return nil - } - - vArray := (*[32]byte)(v) - - return tlv.EBytes32(w, vArray, buf) -} - -// sparseHashDecoder is the customized decoder which skips decoding the empty -// hash. -func sparseHashDecoder(r io.Reader, val interface{}, buf *[8]byte, - l uint64) error { - - v, ok := val.(*SparsePayHash) - if !ok { - return tlv.NewTypeForEncodingErr(val, "SparsePayHash") - } - - // If the length is zero, we will skip encoding the empty hash. - if l == 0 { - return nil - } - - vArray := (*[32]byte)(v) - - return tlv.DBytes32(r, vArray, buf, 32) -} - // HTLCEntry specifies the minimal info needed to be stored on disk for ALL the // historical HTLCs, which is useful for constructing RevocationLog when a // breach is detected. @@ -140,31 +78,6 @@ type HTLCEntry struct { HtlcIndex tlv.OptionalRecordT[tlv.TlvType6, tlv.BigSizeT[uint64]] } -// toTlvStream converts an HTLCEntry record into a tlv representation. -func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { - records := []tlv.Record{ - h.RHash.Record(), - h.RefundTimeout.Record(), - h.OutputIndex.Record(), - h.Incoming.Record(), - h.Amt.Record(), - } - - h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { - records = append(records, r.Record()) - }) - - h.HtlcIndex.WhenSome(func(r tlv.RecordT[tlv.TlvType6, - tlv.BigSizeT[uint64]]) { - - records = append(records, r.Record()) - }) - - tlv.SortRecords(records) - - return tlv.NewStream(records...) -} - // NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC. func NewHTLCEntryFromHTLC(htlc HTLC) (*HTLCEntry, error) { h := &HTLCEntry{ From 69dc5283d9d850d85d040d424f0e54c4f45304b4 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 00:41:09 -0300 Subject: [PATCH 28/61] chanstate: move close summary kv storage Move close summary serialization and closed-channel bucket writes into a dedicated channel state KV store file. Keep the existing channeldb helper names as thin wrappers so current call sites continue to work while the storage implementation moves into chanstate. --- channeldb/channel.go | 154 +---------------------------- chanstate/kv_close_summary.go | 176 ++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 149 deletions(-) create mode 100644 chanstate/kv_close_summary.go diff --git a/channeldb/channel.go b/channeldb/channel.go index dd9f9fd202..063602531e 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2065,161 +2065,17 @@ func (c *ChannelStateDB) RemoteRevocationStore(channel *OpenChannel) ( func putChannelCloseSummary(tx kvdb.RwTx, chanID []byte, summary *ChannelCloseSummary, lastChanState *OpenChannel) error { - closedChanBucket, err := tx.CreateTopLevelBucket(closedChannelBucket) - if err != nil { - return err - } - - summary.RemoteCurrentRevocation = lastChanState.RemoteCurrentRevocation - summary.RemoteNextRevocation = lastChanState.RemoteNextRevocation - summary.LocalChanConfig = lastChanState.LocalChanCfg - - var b bytes.Buffer - if err := serializeChannelCloseSummary(&b, summary); err != nil { - return err - } - - return closedChanBucket.Put(chanID, b.Bytes()) + return cstate.PutChannelCloseSummary( + tx, chanID, summary, lastChanState, + ) } func serializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error { - err := WriteElements(w, - cs.ChanPoint, cs.ShortChanID, cs.ChainHash, cs.ClosingTXID, - cs.CloseHeight, cs.RemotePub, cs.Capacity, cs.SettledBalance, - cs.TimeLockedBalance, cs.CloseType, cs.IsPending, - ) - if err != nil { - return err - } - - // If this is a close channel summary created before the addition of - // the new fields, then we can exit here. - if cs.RemoteCurrentRevocation == nil { - return WriteElements(w, false) - } - - // If fields are present, write boolean to indicate this, and continue. - if err := WriteElements(w, true); err != nil { - return err - } - - if err := WriteElements(w, cs.RemoteCurrentRevocation); err != nil { - return err - } - - if err := writeChanConfig(w, &cs.LocalChanConfig); err != nil { - return err - } - - // The RemoteNextRevocation field is optional, as it's possible for a - // channel to be closed before we learn of the next unrevoked - // revocation point for the remote party. Write a boolean indicating - // whether this field is present or not. - if err := WriteElements(w, cs.RemoteNextRevocation != nil); err != nil { - return err - } - - // Write the field, if present. - if cs.RemoteNextRevocation != nil { - if err = WriteElements(w, cs.RemoteNextRevocation); err != nil { - return err - } - } - - // Write whether the channel sync message is present. - if err := WriteElements(w, cs.LastChanSyncMsg != nil); err != nil { - return err - } - - // Write the channel sync message, if present. - if cs.LastChanSyncMsg != nil { - if err := WriteElements(w, cs.LastChanSyncMsg); err != nil { - return err - } - } - - return nil + return cstate.SerializeChannelCloseSummary(w, cs) } func deserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) { - c := &ChannelCloseSummary{} - - err := ReadElements(r, - &c.ChanPoint, &c.ShortChanID, &c.ChainHash, &c.ClosingTXID, - &c.CloseHeight, &c.RemotePub, &c.Capacity, &c.SettledBalance, - &c.TimeLockedBalance, &c.CloseType, &c.IsPending, - ) - if err != nil { - return nil, err - } - - // We'll now check to see if the channel close summary was encoded with - // any of the additional optional fields. - var hasNewFields bool - err = ReadElements(r, &hasNewFields) - if err != nil { - return nil, err - } - - // If fields are not present, we can return. - if !hasNewFields { - return c, nil - } - - // Otherwise read the new fields. - if err := ReadElements(r, &c.RemoteCurrentRevocation); err != nil { - return nil, err - } - - if err := readChanConfig(r, &c.LocalChanConfig); err != nil { - return nil, err - } - - // Finally, we'll attempt to read the next unrevoked commitment point - // for the remote party. If we closed the channel before receiving a - // channel_ready message then this might not be present. A boolean - // indicating whether the field is present will come first. - var hasRemoteNextRevocation bool - err = ReadElements(r, &hasRemoteNextRevocation) - if err != nil { - return nil, err - } - - // If this field was written, read it. - if hasRemoteNextRevocation { - err = ReadElements(r, &c.RemoteNextRevocation) - if err != nil { - return nil, err - } - } - - // Check if we have a channel sync message to read. - var hasChanSyncMsg bool - err = ReadElements(r, &hasChanSyncMsg) - if err == io.EOF { - return c, nil - } else if err != nil { - return nil, err - } - - // If a chan sync message is present, read it. - if hasChanSyncMsg { - // We must pass in reference to a lnwire.Message for the codec - // to support it. - var msg lnwire.Message - if err := ReadElements(r, &msg); err != nil { - return nil, err - } - - chanSync, ok := msg.(*lnwire.ChannelReestablish) - if !ok { - return nil, errors.New("unable cast db Message to " + - "ChannelReestablish") - } - c.LastChanSyncMsg = chanSync - } - - return c, nil + return cstate.DeserializeCloseChannelSummary(r) } func writeChanConfig(b io.Writer, c *ChannelConfig) error { diff --git a/chanstate/kv_close_summary.go b/chanstate/kv_close_summary.go new file mode 100644 index 0000000000..a11abe2cb4 --- /dev/null +++ b/chanstate/kv_close_summary.go @@ -0,0 +1,176 @@ +package chanstate + +import ( + "bytes" + "errors" + "io" + + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" +) + +// PutChannelCloseSummary writes the immutable close-time summary of a channel +// under the closed channel bucket. +func PutChannelCloseSummary(tx kvdb.RwTx, chanID []byte, + summary *ChannelCloseSummary, lastChanState *OpenChannel) error { + + closedChanBucket, err := tx.CreateTopLevelBucket(closedChannelBucket) + if err != nil { + return err + } + + summary.RemoteCurrentRevocation = lastChanState.RemoteCurrentRevocation + summary.RemoteNextRevocation = lastChanState.RemoteNextRevocation + summary.LocalChanConfig = lastChanState.LocalChanCfg + + var b bytes.Buffer + if err := SerializeChannelCloseSummary(&b, summary); err != nil { + return err + } + + return closedChanBucket.Put(chanID, b.Bytes()) +} + +// SerializeChannelCloseSummary serializes a channel close summary. +func SerializeChannelCloseSummary(w io.Writer, + cs *ChannelCloseSummary) error { + + err := WriteElements(w, + cs.ChanPoint, cs.ShortChanID, cs.ChainHash, cs.ClosingTXID, + cs.CloseHeight, cs.RemotePub, cs.Capacity, cs.SettledBalance, + cs.TimeLockedBalance, cs.CloseType, cs.IsPending, + ) + if err != nil { + return err + } + + // If this is a close channel summary created before the addition of + // the new fields, then we can exit here. + if cs.RemoteCurrentRevocation == nil { + return WriteElements(w, false) + } + + // If fields are present, write boolean to indicate this, and continue. + if err := WriteElements(w, true); err != nil { + return err + } + + if err := WriteElements(w, cs.RemoteCurrentRevocation); err != nil { + return err + } + + if err := WriteChanConfig(w, &cs.LocalChanConfig); err != nil { + return err + } + + // The RemoteNextRevocation field is optional, as it's possible for a + // channel to be closed before we learn of the next unrevoked + // revocation point for the remote party. Write a boolean indicating + // whether this field is present or not. + if err := WriteElements(w, cs.RemoteNextRevocation != nil); err != nil { + return err + } + + // Write the field, if present. + if cs.RemoteNextRevocation != nil { + if err = WriteElements(w, cs.RemoteNextRevocation); err != nil { + return err + } + } + + // Write whether the channel sync message is present. + if err := WriteElements(w, cs.LastChanSyncMsg != nil); err != nil { + return err + } + + // Write the channel sync message, if present. + if cs.LastChanSyncMsg != nil { + if err := WriteElements(w, cs.LastChanSyncMsg); err != nil { + return err + } + } + + return nil +} + +// DeserializeCloseChannelSummary deserializes a channel close summary. +func DeserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) { + c := &ChannelCloseSummary{} + + err := ReadElements(r, + &c.ChanPoint, &c.ShortChanID, &c.ChainHash, &c.ClosingTXID, + &c.CloseHeight, &c.RemotePub, &c.Capacity, &c.SettledBalance, + &c.TimeLockedBalance, &c.CloseType, &c.IsPending, + ) + if err != nil { + return nil, err + } + + // We'll now check to see if the channel close summary was encoded with + // any of the additional optional fields. + var hasNewFields bool + err = ReadElements(r, &hasNewFields) + if err != nil { + return nil, err + } + + // If fields are not present, we can return. + if !hasNewFields { + return c, nil + } + + // Otherwise read the new fields. + if err := ReadElements(r, &c.RemoteCurrentRevocation); err != nil { + return nil, err + } + + if err := ReadChanConfig(r, &c.LocalChanConfig); err != nil { + return nil, err + } + + // Finally, we'll attempt to read the next unrevoked commitment point + // for the remote party. If we closed the channel before receiving a + // channel_ready message then this might not be present. A boolean + // indicating whether the field is present will come first. + var hasRemoteNextRevocation bool + err = ReadElements(r, &hasRemoteNextRevocation) + if err != nil { + return nil, err + } + + // If this field was written, read it. + if hasRemoteNextRevocation { + err = ReadElements(r, &c.RemoteNextRevocation) + if err != nil { + return nil, err + } + } + + // Check if we have a channel sync message to read. + var hasChanSyncMsg bool + err = ReadElements(r, &hasChanSyncMsg) + if errors.Is(err, io.EOF) { + return c, nil + } else if err != nil { + return nil, err + } + + // If a chan sync message is present, read it. + if hasChanSyncMsg { + // We must pass in reference to a lnwire.Message for the codec + // to support it. + var msg lnwire.Message + if err := ReadElements(r, &msg); err != nil { + return nil, err + } + + chanSync, ok := msg.(*lnwire.ChannelReestablish) + if !ok { + return nil, errors.New("unable cast db Message to " + + "ChannelReestablish") + } + c.LastChanSyncMsg = chanSync + } + + return c, nil +} From d5574864784eced4a5d5a959a5554d31fd296a05 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 01:54:11 -0300 Subject: [PATCH 29/61] chanstate: move open channel info storage Move open channel info serialization and upfront shutdown script persistence into the open channel KV store file. Keep channeldb helper names as thin wrappers so the remaining channel linker code can continue using the existing call sites during the move. --- channeldb/channel.go | 152 +------------------------------- chanstate/kv_open_channel.go | 163 +++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 150 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 063602531e..0f08111efe 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -40,8 +40,6 @@ var ( chanIDBucket = cstate.ChanIDBucketKey() historicalChannelBucket = cstate.HistoricalChannelBucketKey() chanInfoKey = cstate.ChanInfoKey() - localUpfrontShutdownKey = cstate.LocalUpfrontShutdownKey() - remoteUpfrontShutdownKey = cstate.RemoteUpfrontShutdownKey() chanCommitmentKey = cstate.ChanCommitmentKey() unsignedAckedUpdatesKey = cstate.UnsignedAckedUpdatesKey() remoteUnsignedLocalUpdatesKey = cstate.RemoteUnsignedLocalUpdatesKey() @@ -2083,91 +2081,7 @@ func writeChanConfig(b io.Writer, c *ChannelConfig) error { } func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - var w bytes.Buffer - if err := WriteElements(&w, - channel.ChanType, channel.ChainHash, channel.FundingOutpoint, - channel.ShortChannelID, channel.IsPending, channel.IsInitiator, - channel.ChannelStatusForStore(), channel.FundingBroadcastHeight, - channel.NumConfsRequired, channel.ChannelFlags, - channel.IdentityPub, channel.Capacity, channel.TotalMSatSent, - channel.TotalMSatReceived, - ); err != nil { - return err - } - - // For single funder channels that we initiated, and we have the - // funding transaction, then write the funding txn. - if channel.FundingTxPresent() { - if err := WriteElement(&w, channel.FundingTxn); err != nil { - return err - } - } - - if err := writeChanConfig(&w, &channel.LocalChanCfg); err != nil { - return err - } - if err := writeChanConfig(&w, &channel.RemoteChanCfg); err != nil { - return err - } - - if err := cstate.EncodeOpenChannelTlvData(&w, channel); err != nil { - return fmt.Errorf("unable to encode aux data: %w", err) - } - - if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil { - return err - } - - // Finally, add optional shutdown scripts for the local and remote peer if - // they are present. - if err := putOptionalUpfrontShutdownScript( - chanBucket, localUpfrontShutdownKey, channel.LocalShutdownScript, - ); err != nil { - return err - } - - return putOptionalUpfrontShutdownScript( - chanBucket, remoteUpfrontShutdownKey, channel.RemoteShutdownScript, - ) -} - -// putOptionalUpfrontShutdownScript adds a shutdown script under the key -// provided if it has a non-zero length. -func putOptionalUpfrontShutdownScript(chanBucket kvdb.RwBucket, key []byte, - script []byte) error { - // If the script is empty, we do not need to add anything. - if len(script) == 0 { - return nil - } - - var w bytes.Buffer - if err := WriteElement(&w, script); err != nil { - return err - } - - return chanBucket.Put(key, w.Bytes()) -} - -// getOptionalUpfrontShutdownScript reads the shutdown script stored under the -// key provided if it is present. Upfront shutdown scripts are optional, so the -// function returns with no error if the key is not present. -func getOptionalUpfrontShutdownScript(chanBucket kvdb.RBucket, key []byte, - script *lnwire.DeliveryAddress) error { - - // Return early if the bucket does not exit, a shutdown script was not set. - bs := chanBucket.Get(key) - if bs == nil { - return nil - } - - var tempScript []byte - r := bytes.NewReader(bs) - if err := ReadElement(r, &tempScript); err != nil { - return err - } - *script = tempScript - - return nil + return cstate.PutChanInfo(chanBucket, channel) } func serializeChanCommit(w io.Writer, c *ChannelCommitment) error { @@ -2243,69 +2157,7 @@ func readChanConfig(b io.Reader, c *ChannelConfig) error { } func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { - infoBytes := chanBucket.Get(chanInfoKey) - if infoBytes == nil { - return ErrNoChanInfoFound - } - r := bytes.NewReader(infoBytes) - - var chanStatus ChannelStatus - if err := ReadElements(r, - &channel.ChanType, &channel.ChainHash, &channel.FundingOutpoint, - &channel.ShortChannelID, &channel.IsPending, &channel.IsInitiator, - &chanStatus, &channel.FundingBroadcastHeight, - &channel.NumConfsRequired, &channel.ChannelFlags, - &channel.IdentityPub, &channel.Capacity, &channel.TotalMSatSent, - &channel.TotalMSatReceived, - ); err != nil { - return err - } - channel.SetChannelStatusForStore(chanStatus) - - // For single funder channels that we initiated and have the funding - // transaction to, read the funding txn. - if channel.FundingTxPresent() { - if err := ReadElement(r, &channel.FundingTxn); err != nil { - return err - } - } - - if err := readChanConfig(r, &channel.LocalChanCfg); err != nil { - return err - } - if err := readChanConfig(r, &channel.RemoteChanCfg); err != nil { - return err - } - - // Retrieve the boolean stored under lastWasRevokeKey. - lastWasRevokeBytes := chanBucket.Get(lastWasRevokeKey) - if lastWasRevokeBytes == nil { - // If nothing has been stored under this key, we store false in the - // OpenChannel struct. - channel.LastWasRevoke = false - } else { - // Otherwise, read the value into the LastWasRevoke field. - revokeReader := bytes.NewReader(lastWasRevokeBytes) - err := ReadElements(revokeReader, &channel.LastWasRevoke) - if err != nil { - return err - } - } - - if err := cstate.DecodeOpenChannelTlvData(r, channel); err != nil { - return fmt.Errorf("unable to decode aux data: %w", err) - } - - // Finally, read the optional shutdown scripts. - if err := getOptionalUpfrontShutdownScript( - chanBucket, localUpfrontShutdownKey, &channel.LocalShutdownScript, - ); err != nil { - return err - } - - return getOptionalUpfrontShutdownScript( - chanBucket, remoteUpfrontShutdownKey, &channel.RemoteShutdownScript, - ) + return cstate.FetchChanInfo(chanBucket, channel) } func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) { diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index 1e3ca1d487..ed214ae3e6 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -352,6 +352,169 @@ func ReadChanConfig(b io.Reader, c *ChannelConfig) error { ) } +// PutChanInfo serializes the static channel info into the channel bucket. +func PutChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { + var w bytes.Buffer + if err := WriteElements(&w, + channel.ChanType, channel.ChainHash, channel.FundingOutpoint, + channel.ShortChannelID, channel.IsPending, channel.IsInitiator, + channel.ChannelStatusForStore(), channel.FundingBroadcastHeight, + channel.NumConfsRequired, channel.ChannelFlags, + channel.IdentityPub, channel.Capacity, channel.TotalMSatSent, + channel.TotalMSatReceived, + ); err != nil { + return err + } + + // For single funder channels that we initiated, and we have the + // funding transaction, then write the funding txn. + if channel.FundingTxPresent() { + if err := WriteElement(&w, channel.FundingTxn); err != nil { + return err + } + } + + if err := WriteChanConfig(&w, &channel.LocalChanCfg); err != nil { + return err + } + if err := WriteChanConfig(&w, &channel.RemoteChanCfg); err != nil { + return err + } + + if err := EncodeOpenChannelTlvData(&w, channel); err != nil { + return fmt.Errorf("unable to encode aux data: %w", err) + } + + if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil { + return err + } + + // Finally, add optional shutdown scripts for the local and remote peer + // if they are present. + if err := putOptionalUpfrontShutdownScript( + chanBucket, localUpfrontShutdownKey, + channel.LocalShutdownScript, + ); err != nil { + return err + } + + return putOptionalUpfrontShutdownScript( + chanBucket, remoteUpfrontShutdownKey, + channel.RemoteShutdownScript, + ) +} + +// putOptionalUpfrontShutdownScript adds a shutdown script under the key +// provided if it has a non-zero length. +func putOptionalUpfrontShutdownScript(chanBucket kvdb.RwBucket, key []byte, + script []byte) error { + + // If the script is empty, we do not need to add anything. + if len(script) == 0 { + return nil + } + + var w bytes.Buffer + if err := WriteElement(&w, script); err != nil { + return err + } + + return chanBucket.Put(key, w.Bytes()) +} + +// FetchChanInfo deserializes the static channel info from the channel bucket. +func FetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { + infoBytes := chanBucket.Get(chanInfoKey) + if infoBytes == nil { + return ErrNoChanInfoFound + } + r := bytes.NewReader(infoBytes) + + var chanStatus ChannelStatus + if err := ReadElements(r, + &channel.ChanType, &channel.ChainHash, &channel.FundingOutpoint, + &channel.ShortChannelID, &channel.IsPending, + &channel.IsInitiator, + &chanStatus, &channel.FundingBroadcastHeight, + &channel.NumConfsRequired, &channel.ChannelFlags, + &channel.IdentityPub, &channel.Capacity, &channel.TotalMSatSent, + &channel.TotalMSatReceived, + ); err != nil { + return err + } + channel.SetChannelStatusForStore(chanStatus) + + // For single funder channels that we initiated and have the funding + // transaction to, read the funding txn. + if channel.FundingTxPresent() { + if err := ReadElement(r, &channel.FundingTxn); err != nil { + return err + } + } + + if err := ReadChanConfig(r, &channel.LocalChanCfg); err != nil { + return err + } + if err := ReadChanConfig(r, &channel.RemoteChanCfg); err != nil { + return err + } + + // Retrieve the boolean stored under lastWasRevokeKey. + lastWasRevokeBytes := chanBucket.Get(lastWasRevokeKey) + if lastWasRevokeBytes == nil { + // If nothing has been stored under this key, we store false + // in the OpenChannel struct. + channel.LastWasRevoke = false + } else { + // Otherwise, read the value into the LastWasRevoke field. + revokeReader := bytes.NewReader(lastWasRevokeBytes) + err := ReadElements(revokeReader, &channel.LastWasRevoke) + if err != nil { + return err + } + } + + if err := DecodeOpenChannelTlvData(r, channel); err != nil { + return fmt.Errorf("unable to decode aux data: %w", err) + } + + // Finally, read the optional shutdown scripts. + if err := getOptionalUpfrontShutdownScript( + chanBucket, localUpfrontShutdownKey, + &channel.LocalShutdownScript, + ); err != nil { + return err + } + + return getOptionalUpfrontShutdownScript( + chanBucket, remoteUpfrontShutdownKey, + &channel.RemoteShutdownScript, + ) +} + +// getOptionalUpfrontShutdownScript reads the shutdown script stored under the +// key provided if it is present. Upfront shutdown scripts are optional, so the +// function returns with no error if the key is not present. +func getOptionalUpfrontShutdownScript(chanBucket kvdb.RBucket, key []byte, + script *lnwire.DeliveryAddress) error { + + // Return early if the bucket does not exit, a shutdown script was not + // set. + bs := chanBucket.Get(key) + if bs == nil { + return nil + } + + var tempScript []byte + r := bytes.NewReader(bs) + if err := ReadElement(r, &tempScript); err != nil { + return err + } + *script = tempScript + + return nil +} + // openChannelTlvData houses the new data fields that are stored for each // channel in a TLV stream within the root bucket. This is stored as a TLV // stream appended to the existing hard-coded fields in the channel's root From 5b0a747c9a0f3577e20f5b312f91c125a531e9df Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 04:45:42 -0300 Subject: [PATCH 30/61] chanstate: move commitment kv storage Move channel commitment and revocation state bucket persistence into the channel state KV commitment file. Keep the existing channeldb helper names as thin wrappers so current call sites continue to work while the storage implementation moves into chanstate. --- channeldb/channel.go | 153 ++---------------------------- chanstate/kv_commitment.go | 187 +++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 146 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 0f08111efe..30cd3c278a 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -39,11 +39,8 @@ var ( outpointBucket = cstate.OutpointBucketKey() chanIDBucket = cstate.ChanIDBucketKey() historicalChannelBucket = cstate.HistoricalChannelBucketKey() - chanInfoKey = cstate.ChanInfoKey() - chanCommitmentKey = cstate.ChanCommitmentKey() unsignedAckedUpdatesKey = cstate.UnsignedAckedUpdatesKey() remoteUnsignedLocalUpdatesKey = cstate.RemoteUnsignedLocalUpdatesKey() - revocationStateKey = cstate.RevocationStateKey() commitDiffKey = cstate.CommitDiffKey() lastWasRevokeKey = cstate.LastWasRevokeKey() ) @@ -2091,65 +2088,15 @@ func serializeChanCommit(w io.Writer, c *ChannelCommitment) error { func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, local bool) error { - var commitKey []byte - if local { - commitKey = append(chanCommitmentKey, byte(0x00)) - } else { - commitKey = append(chanCommitmentKey, byte(0x01)) - } - - var b bytes.Buffer - if err := cstate.SerializeChanCommit(&b, c); err != nil { - return err - } - - // Before we write to disk, we'll also write our aux data as well. - if err := cstate.EncodeCommitTlvData(&b, c); err != nil { - return fmt.Errorf("unable to write aux data: %w", err) - } - - return chanBucket.Put(commitKey, b.Bytes()) + return cstate.PutChanCommitment(chanBucket, c, local) } func putChanCommitments(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - // If this is a restored channel, then we don't have any commitments to - // write. - if channel.HasChanStatusForStore(ChanStatusRestored) { - return nil - } - - err := putChanCommitment( - chanBucket, &channel.LocalCommitment, true, - ) - if err != nil { - return err - } - - return putChanCommitment( - chanBucket, &channel.RemoteCommitment, false, - ) + return cstate.PutChanCommitments(chanBucket, channel) } func putChanRevocationState(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - var b bytes.Buffer - err := WriteElements( - &b, channel.RemoteCurrentRevocation, channel.RevocationProducer, - channel.RevocationStore, - ) - if err != nil { - return err - } - - // If the next revocation is present, which is only the case after the - // ChannelReady message has been sent, then we'll write it to disk. - if channel.RemoteNextRevocation != nil { - err = WriteElements(&b, channel.RemoteNextRevocation) - if err != nil { - return err - } - } - - return chanBucket.Put(revocationStateKey, b.Bytes()) + return cstate.PutChanRevocationState(chanBucket, channel) } func readChanConfig(b io.Reader, c *ChannelConfig) error { @@ -2167,105 +2114,19 @@ func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) { func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment, error) { - var commitKey []byte - if local { - commitKey = append(chanCommitmentKey, byte(0x00)) - } else { - commitKey = append(chanCommitmentKey, byte(0x01)) - } - - commitBytes := chanBucket.Get(commitKey) - if commitBytes == nil { - return ChannelCommitment{}, ErrNoCommitmentsFound - } - - r := bytes.NewReader(commitBytes) - chanCommit, err := cstate.DeserializeChanCommit(r) - if err != nil { - return ChannelCommitment{}, fmt.Errorf("unable to decode "+ - "chan commit: %w", err) - } - - // We'll also check to see if we have any aux data stored as the end of - // the stream. - if err := cstate.DecodeCommitTlvData(r, &chanCommit); err != nil { - return ChannelCommitment{}, fmt.Errorf("unable to decode "+ - "chan aux data: %w", err) - } - - return chanCommit, nil + return cstate.FetchChanCommitment(chanBucket, local) } func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { - var err error - - // If this is a restored channel, then we don't have any commitments to - // read. - if channel.HasChanStatusForStore(ChanStatusRestored) { - return nil - } - - channel.LocalCommitment, err = fetchChanCommitment(chanBucket, true) - if err != nil { - return err - } - channel.RemoteCommitment, err = fetchChanCommitment(chanBucket, false) - if err != nil { - return err - } - - return nil + return cstate.FetchChanCommitments(chanBucket, channel) } func fetchChanRevocationState(chanBucket kvdb.RBucket, channel *OpenChannel) error { - revBytes := chanBucket.Get(revocationStateKey) - if revBytes == nil { - return ErrNoRevocationsFound - } - r := bytes.NewReader(revBytes) - - err := ReadElements( - r, &channel.RemoteCurrentRevocation, &channel.RevocationProducer, - &channel.RevocationStore, - ) - if err != nil { - return err - } - - // If there aren't any bytes left in the buffer, then we don't yet have - // the next remote revocation, so we can exit early here. - if r.Len() == 0 { - return nil - } - - // Otherwise we'll read the next revocation for the remote party which - // is always the last item within the buffer. - return ReadElements(r, &channel.RemoteNextRevocation) + return cstate.FetchChanRevocationState(chanBucket, channel) } func deleteOpenChannel(chanBucket kvdb.RwBucket) error { - if err := chanBucket.Delete(chanInfoKey); err != nil { - return err - } - - err := chanBucket.Delete(append(chanCommitmentKey, byte(0x00))) - if err != nil { - return err - } - err = chanBucket.Delete(append(chanCommitmentKey, byte(0x01))) - if err != nil { - return err - } - - if err := chanBucket.Delete(revocationStateKey); err != nil { - return err - } - - if diff := chanBucket.Get(commitDiffKey); diff != nil { - return chanBucket.Delete(commitDiffKey) - } - - return nil + return cstate.DeleteOpenChannel(chanBucket) } // makeLogKey converts a uint64 into an 8 byte array. diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go index 1ca51f7b2c..c14abb0813 100644 --- a/chanstate/kv_commitment.go +++ b/chanstate/kv_commitment.go @@ -1,6 +1,7 @@ package chanstate import ( + "bytes" "encoding/binary" "fmt" "io" @@ -8,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" ) @@ -230,6 +232,191 @@ func DeserializeChanCommit(r io.Reader) (ChannelCommitment, error) { return c, nil } +func chanCommitKey(local bool) []byte { + commitKey := make([]byte, 0, len(chanCommitmentKey)+1) + commitKey = append(commitKey, chanCommitmentKey...) + if local { + return append(commitKey, byte(0x00)) + } + + return append(commitKey, byte(0x01)) +} + +// PutChanCommitment writes a channel commitment to the channel bucket. +func PutChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, + local bool) error { + + var b bytes.Buffer + if err := SerializeChanCommit(&b, c); err != nil { + return err + } + + // Before we write to disk, we'll also write our aux data as well. + if err := EncodeCommitTlvData(&b, c); err != nil { + return fmt.Errorf("unable to write aux data: %w", err) + } + + return chanBucket.Put(chanCommitKey(local), b.Bytes()) +} + +// PutChanCommitments writes the local and remote commitments to the channel +// bucket. +func PutChanCommitments(chanBucket kvdb.RwBucket, + channel *OpenChannel) error { + + // If this is a restored channel, then we don't have any commitments to + // write. + if channel.HasChanStatusForStore(ChanStatusRestored) { + return nil + } + + err := PutChanCommitment( + chanBucket, &channel.LocalCommitment, true, + ) + if err != nil { + return err + } + + return PutChanCommitment( + chanBucket, &channel.RemoteCommitment, false, + ) +} + +// PutChanRevocationState writes the remote revocation state to the channel +// bucket. +func PutChanRevocationState(chanBucket kvdb.RwBucket, + channel *OpenChannel) error { + + var b bytes.Buffer + err := WriteElements( + &b, channel.RemoteCurrentRevocation, channel.RevocationProducer, + channel.RevocationStore, + ) + if err != nil { + return err + } + + // If the next revocation is present, which is only the case after the + // ChannelReady message has been sent, then we'll write it to disk. + if channel.RemoteNextRevocation != nil { + err = WriteElements(&b, channel.RemoteNextRevocation) + if err != nil { + return err + } + } + + return chanBucket.Put(revocationStateKey, b.Bytes()) +} + +// FetchChanCommitment reads a channel commitment from the channel bucket. +func FetchChanCommitment(chanBucket kvdb.RBucket, + local bool) (ChannelCommitment, error) { + + commitBytes := chanBucket.Get(chanCommitKey(local)) + if commitBytes == nil { + return ChannelCommitment{}, ErrNoCommitmentsFound + } + + r := bytes.NewReader(commitBytes) + chanCommit, err := DeserializeChanCommit(r) + if err != nil { + return ChannelCommitment{}, fmt.Errorf("unable to decode "+ + "chan commit: %w", err) + } + + // We'll also check to see if we have any aux data stored as the end of + // the stream. + if err := DecodeCommitTlvData(r, &chanCommit); err != nil { + return ChannelCommitment{}, fmt.Errorf("unable to decode "+ + "chan aux data: %w", err) + } + + return chanCommit, nil +} + +// FetchChanCommitments reads the local and remote commitments from the channel +// bucket. +func FetchChanCommitments(chanBucket kvdb.RBucket, + channel *OpenChannel) error { + + var err error + + // If this is a restored channel, then we don't have any commitments to + // read. + if channel.HasChanStatusForStore(ChanStatusRestored) { + return nil + } + + channel.LocalCommitment, err = FetchChanCommitment(chanBucket, true) + if err != nil { + return err + } + channel.RemoteCommitment, err = FetchChanCommitment(chanBucket, false) + if err != nil { + return err + } + + return nil +} + +// FetchChanRevocationState reads the remote revocation state from the channel +// bucket. +func FetchChanRevocationState(chanBucket kvdb.RBucket, + channel *OpenChannel) error { + + revBytes := chanBucket.Get(revocationStateKey) + if revBytes == nil { + return ErrNoRevocationsFound + } + r := bytes.NewReader(revBytes) + + err := ReadElements( + r, + &channel.RemoteCurrentRevocation, &channel.RevocationProducer, + &channel.RevocationStore, + ) + if err != nil { + return err + } + + // If there aren't any bytes left in the buffer, then we don't yet have + // the next remote revocation, so we can exit early here. + if r.Len() == 0 { + return nil + } + + // Otherwise we'll read the next revocation for the remote party which + // is always the last item within the buffer. + return ReadElements(r, &channel.RemoteNextRevocation) +} + +// DeleteOpenChannel deletes the persisted open channel state from the channel +// bucket. +func DeleteOpenChannel(chanBucket kvdb.RwBucket) error { + if err := chanBucket.Delete(chanInfoKey); err != nil { + return err + } + + err := chanBucket.Delete(chanCommitKey(true)) + if err != nil { + return err + } + err = chanBucket.Delete(chanCommitKey(false)) + if err != nil { + return err + } + + if err := chanBucket.Delete(revocationStateKey); err != nil { + return err + } + + if diff := chanBucket.Get(commitDiffKey); diff != nil { + return chanBucket.Delete(commitDiffKey) + } + + return nil +} + // commitTlvData stores all the optional data that may be stored as a TLV stream // at the _end_ of the normal serialized commit on disk. type commitTlvData struct { From dcad151b5ffc58c51f750c63a35f44f201297a3e Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:01:24 -0300 Subject: [PATCH 31/61] chanstate: move open channel kv storage Move complete open channel read and write composition into the channel state KV open channel file. Keep the channeldb helper names as wrappers so existing store methods can continue to call through while their implementations move in later commits. --- channeldb/channel.go | 70 +----------------------------- chanstate/kv_open_channel.go | 84 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 68 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 30cd3c278a..207edffdbd 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -874,37 +874,7 @@ func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, // putOpenChannel serializes, and stores the current state of the channel in its // entirety. func putOpenChannel(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - // First, we'll write out all the relatively static fields, that are - // decided upon initial channel creation. - if err := putChanInfo(chanBucket, channel); err != nil { - return fmt.Errorf("unable to store chan info: %w", err) - } - - // With the static channel info written out, we'll now write out the - // current commitment state for both parties. - if err := putChanCommitments(chanBucket, channel); err != nil { - return fmt.Errorf("unable to store chan commitments: %w", err) - } - - // Next, if this is a frozen channel, we'll add in the axillary - // information we need to store. - if channel.ChanType.IsFrozen() || channel.ChanType.HasLeaseExpiration() { - err := storeThawHeight( - chanBucket, channel.ThawHeight, - ) - if err != nil { - return fmt.Errorf("unable to store thaw height: %w", - err) - } - } - - // Finally, we'll write out the revocation state for both parties - // within a distinct key space. - if err := putChanRevocationState(chanBucket, channel); err != nil { - return fmt.Errorf("unable to store chan revocations: %w", err) - } - - return nil + return cstate.PutOpenChannel(chanBucket, channel) } // fetchOpenChannel retrieves, and deserializes (including decrypting @@ -912,43 +882,7 @@ func putOpenChannel(chanBucket kvdb.RwBucket, channel *OpenChannel) error { func fetchOpenChannel(chanBucket kvdb.RBucket, chanPoint *wire.OutPoint) (*OpenChannel, error) { - channel := &OpenChannel{ - FundingOutpoint: *chanPoint, - } - - // First, we'll read all the static information that changes less - // frequently from disk. - if err := fetchChanInfo(chanBucket, channel); err != nil { - return nil, fmt.Errorf("unable to fetch chan info: %w", err) - } - - // With the static information read, we'll now read the current - // commitment state for both sides of the channel. - if err := fetchChanCommitments(chanBucket, channel); err != nil { - return nil, fmt.Errorf("unable to fetch chan commitments: %w", - err) - } - - // Next, if this is a frozen channel, we'll add in the axillary - // information we need to store. - if channel.ChanType.IsFrozen() || channel.ChanType.HasLeaseExpiration() { - thawHeight, err := fetchThawHeight(chanBucket) - if err != nil { - return nil, fmt.Errorf("unable to store thaw "+ - "height: %v", err) - } - - channel.ThawHeight = thawHeight - } - - // Finally, we'll retrieve the current revocation state so we can - // properly - if err := fetchChanRevocationState(chanBucket, channel); err != nil { - return nil, fmt.Errorf("unable to fetch chan revocations: %w", - err) - } - - return channel, nil + return cstate.FetchOpenChannel(chanBucket, chanPoint) } // SyncPendingChannel writes a pending channel to the store and records the diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index ed214ae3e6..0da9bd8921 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -515,6 +515,90 @@ func getOptionalUpfrontShutdownScript(chanBucket kvdb.RBucket, key []byte, return nil } +// PutOpenChannel serializes, and stores the current state of the channel in +// its entirety. +func PutOpenChannel(chanBucket kvdb.RwBucket, channel *OpenChannel) error { + // First, we'll write out all the relatively static fields, that are + // decided upon initial channel creation. + if err := PutChanInfo(chanBucket, channel); err != nil { + return fmt.Errorf("unable to store chan info: %w", err) + } + + // With the static channel info written out, we'll now write out the + // current commitment state for both parties. + if err := PutChanCommitments(chanBucket, channel); err != nil { + return fmt.Errorf("unable to store chan commitments: %w", err) + } + + // Next, if this is a frozen channel, we'll add in the axillary + // information we need to store. + if channel.ChanType.IsFrozen() || + channel.ChanType.HasLeaseExpiration() { + + err := StoreThawHeight( + chanBucket, channel.ThawHeight, + ) + if err != nil { + return fmt.Errorf("unable to store thaw height: %w", + err) + } + } + + // Finally, we'll write out the revocation state for both parties + // within a distinct key space. + if err := PutChanRevocationState(chanBucket, channel); err != nil { + return fmt.Errorf("unable to store chan revocations: %w", err) + } + + return nil +} + +// FetchOpenChannel retrieves, and deserializes (including decrypting +// sensitive) the complete channel currently active with the passed nodeID. +func FetchOpenChannel(chanBucket kvdb.RBucket, + chanPoint *wire.OutPoint) (*OpenChannel, error) { + + channel := &OpenChannel{ + FundingOutpoint: *chanPoint, + } + + // First, we'll read all the static information that changes less + // frequently from disk. + if err := FetchChanInfo(chanBucket, channel); err != nil { + return nil, fmt.Errorf("unable to fetch chan info: %w", err) + } + + // With the static information read, we'll now read the current + // commitment state for both sides of the channel. + if err := FetchChanCommitments(chanBucket, channel); err != nil { + return nil, fmt.Errorf("unable to fetch chan commitments: %w", + err) + } + + // Next, if this is a frozen channel, we'll add in the axillary + // information we need to store. + if channel.ChanType.IsFrozen() || + channel.ChanType.HasLeaseExpiration() { + + thawHeight, err := FetchThawHeight(chanBucket) + if err != nil { + return nil, fmt.Errorf("unable to store thaw "+ + "height: %v", err) + } + + channel.ThawHeight = thawHeight + } + + // Finally, we'll retrieve the current revocation state so we can + // properly + if err := FetchChanRevocationState(chanBucket, channel); err != nil { + return nil, fmt.Errorf("unable to fetch chan revocations: %w", + err) + } + + return channel, nil +} + // openChannelTlvData houses the new data fields that are stored for each // channel in a TLV stream within the root bucket. This is stored as a TLV // stream appended to the existing hard-coded fields in the channel's root From a2ba302b6ba7df0719707dfd3eba741af01f3f4e Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:03:58 -0300 Subject: [PATCH 32/61] chanstate: move channel exists error Move ErrChanAlreadyExists into chanstate with its existing documentation so channel state KV helpers can return it directly. Leave channeldb exporting the same error through an alias to preserve existing callers while bucket creation code moves packages. --- channeldb/error.go | 2 +- chanstate/errors.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/channeldb/error.go b/channeldb/error.go index d55be66cb6..ce1949f66c 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -55,5 +55,5 @@ var ( // ErrChanAlreadyExists is return when the caller attempts to create a // channel with a channel point that is already present in the // database. - ErrChanAlreadyExists = fmt.Errorf("channel already exists") + ErrChanAlreadyExists = cstate.ErrChanAlreadyExists ) diff --git a/chanstate/errors.go b/chanstate/errors.go index 54e4855470..fe6ffce346 100644 --- a/chanstate/errors.go +++ b/chanstate/errors.go @@ -22,6 +22,11 @@ var ( // for a specific chain, but it is not found. ErrChannelNotFound = fmt.Errorf("channel not found") + // ErrChanAlreadyExists is return when the caller attempts to create a + // channel with a channel point that is already present in the + // database. + ErrChanAlreadyExists = fmt.Errorf("channel already exists") + // ErrNoRevocationsFound is returned when revocation state for a // particular channel cannot be found. ErrNoRevocationsFound = fmt.Errorf("no revocations found") From 98a6f6e5f8bbbb2e4367fb9551dbf643528c17dd Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:15:36 -0300 Subject: [PATCH 33/61] chanstate: move open channel bucket sync Move the full open channel bucket creation and index sync helper into the channel state KV bucket file. Keep channeldb delegating through its existing helper name while the remaining store methods are moved in later commits. --- channeldb/channel.go | 91 ++++-------------------------------- chanstate/kv_open_channel.go | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 82 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 207edffdbd..45325b6416 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -300,7 +300,8 @@ func (c *ChannelStateDB) RefreshChannel(channel *OpenChannel) error { // channel's data resides in given: the public key for the node, the outpoint, // and the chainhash that the channel resides on. func fetchChanBucket(tx kvdb.RTx, nodeKey *btcec.PublicKey, - outPoint *wire.OutPoint, chainHash chainhash.Hash) (kvdb.RBucket, error) { + outPoint *wire.OutPoint, chainHash chainhash.Hash) ( + kvdb.RBucket, error) { return cstate.FetchChanBucket(tx, nodeKey, outPoint, chainHash) } @@ -325,80 +326,7 @@ func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, // fullSyncOpenChannel syncs the contents of an OpenChannel while re-using an // existing database transaction. func fullSyncOpenChannel(tx kvdb.RwTx, c *OpenChannel) error { - // Fetch the outpoint bucket and check if the outpoint already exists. - opBucket := tx.ReadWriteBucket(outpointBucket) - if opBucket == nil { - return ErrNoChanDBExists - } - cidBucket := tx.ReadWriteBucket(chanIDBucket) - if cidBucket == nil { - return ErrNoChanDBExists - } - - var chanPointBuf bytes.Buffer - err := graphdb.WriteOutpoint(&chanPointBuf, &c.FundingOutpoint) - if err != nil { - return err - } - - // Now, check if the outpoint exists in our index. - if opBucket.Get(chanPointBuf.Bytes()) != nil { - return ErrChanAlreadyExists - } - - cid := lnwire.NewChanIDFromOutPoint(c.FundingOutpoint) - if cidBucket.Get(cid[:]) != nil { - return ErrChanAlreadyExists - } - - // Add the outpoint to our outpoint index with the tlv stream. - if err := cstate.PutOpenOutpointIndex( - opBucket, chanPointBuf.Bytes(), - ); err != nil { - return err - } - - if err := cidBucket.Put(cid[:], []byte{}); err != nil { - return err - } - - // First fetch the top level bucket which stores all data related to - // current, active channels. - openChanBucket, err := tx.CreateTopLevelBucket(openChannelBucket) - if err != nil { - return err - } - - // Within this top level bucket, fetch the bucket dedicated to storing - // open channel data specific to the remote node. - nodePub := c.IdentityPub.SerializeCompressed() - nodeChanBucket, err := openChanBucket.CreateBucketIfNotExists(nodePub) - if err != nil { - return err - } - - // We'll then recurse down an additional layer in order to fetch the - // bucket for this particular chain. - chainBucket, err := nodeChanBucket.CreateBucketIfNotExists(c.ChainHash[:]) - if err != nil { - return err - } - - // With the bucket for the node fetched, we can now go down another - // level, creating the bucket for this channel itself. - chanBucket, err := chainBucket.CreateBucket( - chanPointBuf.Bytes(), - ) - switch { - case err == kvdb.ErrBucketExists: - // If this channel already exists, then in order to avoid - // overriding it, we'll return an error back up to the caller. - return ErrChanAlreadyExists - case err != nil: - return err - } - - return putOpenChannel(chanBucket, c) + return cstate.FullSyncOpenChannel(tx, c) } // MarkChannelConfirmationHeight updates the channel's confirmation height once @@ -754,9 +682,12 @@ func (c *ChannelStateDB) getClosingTx(channel *OpenChannel, tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, ) - switch err { - case nil: - case ErrNoChanDBExists, ErrNoActiveChannels, ErrChannelNotFound: + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + return ErrNoCloseTx default: return err @@ -2007,10 +1938,6 @@ func deserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) { return cstate.DeserializeCloseChannelSummary(r) } -func writeChanConfig(b io.Writer, c *ChannelConfig) error { - return cstate.WriteChanConfig(b, c) -} - func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { return cstate.PutChanInfo(chanBucket, channel) } diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index 0da9bd8921..f13e8cf091 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -2,6 +2,7 @@ package chanstate import ( "bytes" + "errors" "fmt" "io" @@ -282,6 +283,87 @@ func FetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey, return chanBucket, nil } +// FullSyncOpenChannel syncs the contents of an OpenChannel while re-using an +// existing database transaction. +func FullSyncOpenChannel(tx kvdb.RwTx, c *OpenChannel) error { + // Fetch the outpoint bucket and check if the outpoint already exists. + opBucket := tx.ReadWriteBucket(outpointBucket) + if opBucket == nil { + return ErrNoChanDBExists + } + cidBucket := tx.ReadWriteBucket(chanIDBucket) + if cidBucket == nil { + return ErrNoChanDBExists + } + + var chanPointBuf bytes.Buffer + err := graphdb.WriteOutpoint(&chanPointBuf, &c.FundingOutpoint) + if err != nil { + return err + } + + // Now, check if the outpoint exists in our index. + if opBucket.Get(chanPointBuf.Bytes()) != nil { + return ErrChanAlreadyExists + } + + cid := lnwire.NewChanIDFromOutPoint(c.FundingOutpoint) + if cidBucket.Get(cid[:]) != nil { + return ErrChanAlreadyExists + } + + // Add the outpoint to our outpoint index with the tlv stream. + if err := PutOpenOutpointIndex( + opBucket, chanPointBuf.Bytes(), + ); err != nil { + return err + } + + if err := cidBucket.Put(cid[:], []byte{}); err != nil { + return err + } + + // First fetch the top level bucket which stores all data related to + // current, active channels. + openChanBucket, err := tx.CreateTopLevelBucket(openChannelBucket) + if err != nil { + return err + } + + // Within this top level bucket, fetch the bucket dedicated to storing + // open channel data specific to the remote node. + nodePub := c.IdentityPub.SerializeCompressed() + nodeChanBucket, err := openChanBucket.CreateBucketIfNotExists(nodePub) + if err != nil { + return err + } + + // We'll then recurse down an additional layer in order to fetch the + // bucket for this particular chain. + chainBucket, err := nodeChanBucket.CreateBucketIfNotExists( + c.ChainHash[:], + ) + if err != nil { + return err + } + + // With the bucket for the node fetched, we can now go down another + // level, creating the bucket for this channel itself. + chanBucket, err := chainBucket.CreateBucket( + chanPointBuf.Bytes(), + ) + switch { + case errors.Is(err, kvdb.ErrBucketExists): + // If this channel already exists, then in order to avoid + // overriding it, we'll return an error back up to the caller. + return ErrChanAlreadyExists + case err != nil: + return err + } + + return PutOpenChannel(chanBucket, c) +} + // keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the // tlv.RecordProducer interface. type keyLocRecord struct { From e08b24ae260a1ed17f33cdbeeb262a916611dae8 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:18:06 -0300 Subject: [PATCH 34/61] chanstate: move borked channel check Move the borked-channel bucket check into the channel state KV open channel file with its existing documentation. Keep channeldb using the same helper name as a wrapper so commitment update paths continue to call through unchanged. --- channeldb/channel.go | 9 +-------- chanstate/kv_open_channel.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 45325b6416..b59eea09ed 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -591,14 +591,7 @@ func (c *ChannelStateDB) FetchChannelShutdownInfo( func isChannelBorked(channel *OpenChannel, chanBucket kvdb.RBucket) ( bool, error) { - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return false, err - } - - return diskChannel.ChannelStatusForStore() != ChanStatusDefault, nil + return cstate.IsChannelBorked(channel, chanBucket) } // MarkChannelCommitmentBroadcasted marks the channel as having a commitment diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index f13e8cf091..22e723a1d8 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -681,6 +681,24 @@ func FetchOpenChannel(chanBucket kvdb.RBucket, return channel, nil } +// IsChannelBorked returns true if the channel has been marked as borked in the +// database. This requires an existing database transaction to already be +// active. +// +// NOTE: The primary mutex should already be held before this method is called. +func IsChannelBorked(channel *OpenChannel, chanBucket kvdb.RBucket) ( + bool, error) { + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return false, err + } + + return diskChannel.ChannelStatusForStore() != ChanStatusDefault, nil +} + // openChannelTlvData houses the new data fields that are stored for each // channel in a TLV stream within the root bucket. This is stored as a TLV // stream appended to the existing hard-coded fields in the channel's root From b4e658f6dc8e650fff48a6f135d3b72d9eb78bce Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:21:05 -0300 Subject: [PATCH 35/61] chanstate: move open channel metadata updates Move simple open channel metadata update paths into the channel state KV open channel file. Keep the ChannelStateDB methods in channeldb as wrappers while the backend-specific transaction logic shifts into chanstate. --- channeldb/channel.go | 108 ++-------------------------- chanstate/kv_open_channel.go | 135 +++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 101 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index b59eea09ed..74c37d864c 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -334,26 +334,7 @@ func fullSyncOpenChannel(tx kvdb.RwTx, c *OpenChannel) error { func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, height uint32) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - diskChannel.ConfirmationHeight = height - - return putOpenChannel(chanBucket, diskChannel) - }, func() {}) + return cstate.MarkChannelConfirmationHeight(c.backend, channel, height) } // MarkChannelCloseConfirmationHeight updates the channel's close confirmation @@ -361,26 +342,9 @@ func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, func (c *ChannelStateDB) MarkChannelCloseConfirmationHeight( channel *OpenChannel, height fn.Option[uint32]) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - diskChannel.CloseConfirmationHeight = height - - return putOpenChannel(chanBucket, diskChannel) - }, func() {}) + return cstate.MarkChannelCloseConfirmationHeight( + c.backend, channel, height, + ) } // MarkChannelOpen marks a channel as fully open given a locator that uniquely @@ -388,53 +352,14 @@ func (c *ChannelStateDB) MarkChannelCloseConfirmationHeight( func (c *ChannelStateDB) MarkChannelOpen(channel *OpenChannel, openLoc lnwire.ShortChannelID) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - diskChannel.IsPending = false - diskChannel.ShortChannelID = openLoc - - return putOpenChannel(chanBucket, diskChannel) - }, func() {}) + return cstate.MarkChannelOpen(c.backend, channel, openLoc) } // MarkChannelRealScid marks the zero-conf channel's confirmed ShortChannelID. func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, realScid lnwire.ShortChannelID) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - diskChannel.SetConfirmedScidForStore(realScid) - - return putOpenChannel(chanBucket, diskChannel) - }, func() {}) + return cstate.MarkChannelRealScid(c.backend, channel, realScid) } // MarkChannelScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in the @@ -442,26 +367,7 @@ func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( channel *OpenChannel) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - diskChannel.ChanType |= ScidAliasFeatureBit - - return putOpenChannel(chanBucket, diskChannel) - }, func() {}) + return cstate.MarkChannelScidAliasNegotiated(c.backend, channel) } // MarkChannelDataLoss marks the channel as local-data-loss and stores the diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index 22e723a1d8..2ef410bfe1 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -681,6 +681,141 @@ func FetchOpenChannel(chanBucket kvdb.RBucket, return channel, nil } +// MarkChannelConfirmationHeight updates the channel's confirmation height once +// the channel opening transaction receives one confirmation. +func MarkChannelConfirmationHeight(backend kvdb.Backend, + channel *OpenChannel, height uint32) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + diskChannel.ConfirmationHeight = height + + return PutOpenChannel(chanBucket, diskChannel) + }, func() {}) +} + +// MarkChannelCloseConfirmationHeight updates the channel's close confirmation +// height when the closing transaction is first detected in a block. +func MarkChannelCloseConfirmationHeight(backend kvdb.Backend, + channel *OpenChannel, height fn.Option[uint32]) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + diskChannel.CloseConfirmationHeight = height + + return PutOpenChannel(chanBucket, diskChannel) + }, func() {}) +} + +// MarkChannelOpen marks a channel as fully open given a locator that uniquely +// describes its location within the chain. +func MarkChannelOpen(backend kvdb.Backend, channel *OpenChannel, + openLoc lnwire.ShortChannelID) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + diskChannel.IsPending = false + diskChannel.ShortChannelID = openLoc + + return PutOpenChannel(chanBucket, diskChannel) + }, func() {}) +} + +// MarkChannelRealScid marks the zero-conf channel's confirmed ShortChannelID. +func MarkChannelRealScid(backend kvdb.Backend, channel *OpenChannel, + realScid lnwire.ShortChannelID) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + diskChannel.SetConfirmedScidForStore(realScid) + + return PutOpenChannel(chanBucket, diskChannel) + }, func() {}) +} + +// MarkChannelScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in the +// database. +func MarkChannelScidAliasNegotiated(backend kvdb.Backend, + channel *OpenChannel) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + diskChannel.ChanType |= ScidAliasFeatureBit + + return PutOpenChannel(chanBucket, diskChannel) + }, func() {}) +} + // IsChannelBorked returns true if the channel has been marked as borked in the // database. This requires an existing database transaction to already be // active. From 617adc76574cf0ba085d2f7c3da659738352dec5 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:24:08 -0300 Subject: [PATCH 36/61] chanstate: move open channel status updates Move open channel status update logic into the channel state KV open channel file, including the callback-capable helper used by related status paths. Keep channeldb methods delegating through their existing names while transaction and bucket logic continues moving into chanstate. --- channeldb/channel.go | 77 ++--------------------------- chanstate/kv_open_channel.go | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 74 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 74c37d864c..76a0af2d87 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -610,7 +610,7 @@ func (c *ChannelStateDB) getClosingTx(channel *OpenChannel, func (c *ChannelStateDB) ApplyChannelStatus(channel *OpenChannel, status ChannelStatus) error { - return c.putChanStatus(channel, status) + return cstate.ApplyChannelStatus(c.backend, channel, status) } // putChanStatus appends the given status to the channel. fs is an optional list @@ -619,50 +619,7 @@ func (c *ChannelStateDB) ApplyChannelStatus(channel *OpenChannel, func (c *ChannelStateDB) putChanStatus(channel *OpenChannel, status ChannelStatus, fs ...func(kvdb.RwBucket) error) error { - if err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - // Add this status to the existing bitvector found in the DB. - status = diskChannel.ChannelStatusForStore() | status - diskChannel.SetChannelStatusForStore(status) - - if err := putOpenChannel(chanBucket, diskChannel); err != nil { - return err - } - - for _, f := range fs { - // Skip execution of nil closures. - if f == nil { - continue - } - - if err := f(chanBucket); err != nil { - return err - } - } - - return nil - }, func() {}); err != nil { - return err - } - - // Update the in-memory representation to keep it in sync with the DB. - channel.SetChannelStatusForStore(status) - - return nil + return cstate.PutChanStatus(c.backend, channel, status, fs...) } // ClearChannelStatus clears the target status from the channel's persisted @@ -670,35 +627,7 @@ func (c *ChannelStateDB) putChanStatus(channel *OpenChannel, func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, status ChannelStatus) error { - if err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - diskChannel, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - // Unset this bit in the bitvector on disk. - status = diskChannel.ChannelStatusForStore() & ^status - diskChannel.SetChannelStatusForStore(status) - - return putOpenChannel(chanBucket, diskChannel) - }, func() {}); err != nil { - return err - } - - // Update the in-memory representation to keep it in sync with the DB. - channel.SetChannelStatusForStore(status) - - return nil + return cstate.ClearChannelStatus(c.backend, channel, status) } // putOpenChannel serializes, and stores the current state of the channel in its diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index 2ef410bfe1..d7e6092297 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -816,6 +816,102 @@ func MarkChannelScidAliasNegotiated(backend kvdb.Backend, }, func() {}) } +// ApplyChannelStatus adds the target status to the channel's persisted status +// bit field. +func ApplyChannelStatus(backend kvdb.Backend, channel *OpenChannel, + status ChannelStatus) error { + + return PutChanStatus(backend, channel, status) +} + +// PutChanStatus appends the given status to the channel. fs is an optional +// list of closures that are given the chanBucket in order to atomically add +// extra information together with the new status. +func PutChanStatus(backend kvdb.Backend, channel *OpenChannel, + status ChannelStatus, fs ...func(kvdb.RwBucket) error) error { + + if err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + // Add this status to the existing bitvector found in the DB. + status = diskChannel.ChannelStatusForStore() | status + diskChannel.SetChannelStatusForStore(status) + + if err := PutOpenChannel(chanBucket, diskChannel); err != nil { + return err + } + + for _, f := range fs { + // Skip execution of nil closures. + if f == nil { + continue + } + + if err := f(chanBucket); err != nil { + return err + } + } + + return nil + }, func() {}); err != nil { + return err + } + + // Update the in-memory representation to keep it in sync with the DB. + channel.SetChannelStatusForStore(status) + + return nil +} + +// ClearChannelStatus clears the target status from the channel's persisted +// status bit field. +func ClearChannelStatus(backend kvdb.Backend, channel *OpenChannel, + status ChannelStatus) error { + + if err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + diskChannel, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + // Unset this bit in the bitvector on disk. + status = diskChannel.ChannelStatusForStore() & ^status + diskChannel.SetChannelStatusForStore(status) + + return PutOpenChannel(chanBucket, diskChannel) + }, func() {}); err != nil { + return err + } + + // Update the in-memory representation to keep it in sync with the DB. + channel.SetChannelStatusForStore(status) + + return nil +} + // IsChannelBorked returns true if the channel has been marked as borked in the // database. This requires an existing database transaction to already be // active. From 085aedf134f808affd1a3fa81f799413e1967cca Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:26:36 -0300 Subject: [PATCH 37/61] chanstate: move data loss store methods Move the channel data-loss status and commit-point fetch paths into the channel state KV data-loss file. Keep channeldb methods as wrappers so callers continue using the existing Store interface while KV transaction logic moves into chanstate. --- channeldb/channel.go | 40 ++--------------------------- chanstate/kv_open_channel.go | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 38 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 76a0af2d87..9481dd0b41 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -375,13 +375,7 @@ func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, commitPoint *btcec.PublicKey) error { - putCommitPoint := func(chanBucket kvdb.RwBucket) error { - return cstate.PutChannelDataLossCommitPoint( - chanBucket, commitPoint, - ) - } - - return c.putChanStatus(channel, ChanStatusLocalDataLoss, putCommitPoint) + return cstate.MarkChannelDataLoss(c.backend, channel, commitPoint) } // FetchChannelDataLossCommitPoint retrieves the commit point stored when the @@ -389,37 +383,7 @@ func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, func (c *ChannelStateDB) FetchChannelDataLossCommitPoint( channel *OpenChannel) (*btcec.PublicKey, error) { - var commitPoint *btcec.PublicKey - - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - switch { - case err == nil: - case errors.Is(err, ErrNoChanDBExists), - errors.Is(err, ErrNoActiveChannels), - errors.Is(err, ErrChannelNotFound): - - return ErrNoCommitPoint - default: - return err - } - - commitPoint, err = cstate.FetchChannelDataLossCommitPoint( - chanBucket, - ) - - return err - }, func() { - commitPoint = nil - }) - if err != nil { - return nil, err - } - - return commitPoint, nil + return cstate.FetchDataLossCommitPoint(c.backend, channel) } // MarkChannelBorked marks the channel as irreconcilable. diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index d7e6092297..e4e9179470 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -824,6 +824,56 @@ func ApplyChannelStatus(backend kvdb.Backend, channel *OpenChannel, return PutChanStatus(backend, channel, status) } +// MarkChannelDataLoss marks the channel as local-data-loss and stores the +// commit point needed if the remote force closes. +func MarkChannelDataLoss(backend kvdb.Backend, channel *OpenChannel, + commitPoint *btcec.PublicKey) error { + + putCommitPoint := func(chanBucket kvdb.RwBucket) error { + return PutChannelDataLossCommitPoint(chanBucket, commitPoint) + } + + return PutChanStatus( + backend, channel, ChanStatusLocalDataLoss, putCommitPoint, + ) +} + +// FetchDataLossCommitPoint retrieves the commit point stored when the channel +// was marked as local-data-loss. +func FetchDataLossCommitPoint(backend kvdb.Backend, + channel *OpenChannel) (*btcec.PublicKey, error) { + + var commitPoint *btcec.PublicKey + + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + + return ErrNoCommitPoint + default: + return err + } + + commitPoint, err = FetchChannelDataLossCommitPoint(chanBucket) + + return err + }, func() { + commitPoint = nil + }) + if err != nil { + return nil, err + } + + return commitPoint, nil +} + // PutChanStatus appends the given status to the channel. fs is an optional // list of closures that are given the chanBucket in order to atomically add // extra information together with the new status. From 59b51e8dae6f0712a932c38894aa1e1a25b4ccd0 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:31:12 -0300 Subject: [PATCH 38/61] chanstate: move close tx store methods Move close transaction status updates and close transaction fetch logic into the channel state KV close transaction file. Keep channeldb methods delegating to chanstate while preserving the public store surface used by channel callers. --- channeldb/channel.go | 79 +++------------------------------- chanstate/kv_close_tx.go | 93 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 74 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 9481dd0b41..b5db9ebe8a 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -470,9 +470,8 @@ func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { - return c.markBroadcasted( - channel, ChanStatusCommitBroadcasted, cstate.ForceCloseTxKey(), - closeTx, closer, + return cstate.MarkChannelCommitmentBroadcasted( + c.backend, channel, closeTx, closer, ) } @@ -481,42 +480,11 @@ func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( func (c *ChannelStateDB) MarkChannelCoopBroadcasted(channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { - return c.markBroadcasted( - channel, ChanStatusCoopBroadcasted, cstate.CoopCloseTxKey(), - closeTx, closer, + return cstate.MarkChannelCoopBroadcasted( + c.backend, channel, closeTx, closer, ) } -// markBroadcasted modifies the channel status and inserts a close transaction -// under the requested key, which should specify either a coop or force close. -// It adds a status which indicates the party that initiated the channel close. -func (c *ChannelStateDB) markBroadcasted(channel *OpenChannel, - status ChannelStatus, key []byte, closeTx *wire.MsgTx, - closer lntypes.ChannelParty) error { - - if closeTx == nil { - return fmt.Errorf("closeTx must be non-nil") - } - - channel.Lock() - defer channel.Unlock() - - putClosingTx := func(chanBucket kvdb.RwBucket) error { - return cstate.PutChannelCloseTx(chanBucket, key, closeTx) - } - - // Add the initiator status to the status provided. These statuses are - // set in addition to the broadcast status so that we do not need to - // migrate the original logic which does not store initiator. - if closer.IsLocal() { - status |= ChanStatusLocalCloseInitiator - } else { - status |= ChanStatusRemoteCloseInitiator - } - - return c.putChanStatus(channel, status, putClosingTx) -} - // FetchChannelBroadcastedCommitment fetches the stored unilateral closing // transaction. func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( @@ -538,35 +506,7 @@ func (c *ChannelStateDB) FetchChannelBroadcastedCooperative( func (c *ChannelStateDB) getClosingTx(channel *OpenChannel, key []byte) (*wire.MsgTx, error) { - var closeTx *wire.MsgTx - - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - switch { - case err == nil: - case errors.Is(err, ErrNoChanDBExists), - errors.Is(err, ErrNoActiveChannels), - errors.Is(err, ErrChannelNotFound): - - return ErrNoCloseTx - default: - return err - } - - closeTx, err = cstate.FetchChannelCloseTx(chanBucket, key) - - return err - }, func() { - closeTx = nil - }) - if err != nil { - return nil, err - } - - return closeTx, nil + return cstate.FetchClosingTx(c.backend, channel, key) } // ApplyChannelStatus adds the target status to the channel's persisted status @@ -577,15 +517,6 @@ func (c *ChannelStateDB) ApplyChannelStatus(channel *OpenChannel, return cstate.ApplyChannelStatus(c.backend, channel, status) } -// putChanStatus appends the given status to the channel. fs is an optional list -// of closures that are given the chanBucket in order to atomically add extra -// information together with the new status. -func (c *ChannelStateDB) putChanStatus(channel *OpenChannel, - status ChannelStatus, fs ...func(kvdb.RwBucket) error) error { - - return cstate.PutChanStatus(c.backend, channel, status, fs...) -} - // ClearChannelStatus clears the target status from the channel's persisted // status bit field. func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, diff --git a/chanstate/kv_close_tx.go b/chanstate/kv_close_tx.go index 9687ae9d8f..60b34887d8 100644 --- a/chanstate/kv_close_tx.go +++ b/chanstate/kv_close_tx.go @@ -2,9 +2,12 @@ package chanstate import ( "bytes" + "errors" + "fmt" "github.com/btcsuite/btcd/wire/v2" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" ) var ( @@ -60,3 +63,93 @@ func FetchChannelCloseTx(chanBucket kvdb.RBucket, return closeTx, nil } + +// MarkChannelCommitmentBroadcasted marks the channel as having a commitment +// transaction broadcast. +func MarkChannelCommitmentBroadcasted(backend kvdb.Backend, + channel *OpenChannel, closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error { + + return markBroadcasted( + backend, channel, ChanStatusCommitBroadcasted, forceCloseTxKey, + closeTx, closer, + ) +} + +// MarkChannelCoopBroadcasted marks the channel as having a cooperative close +// transaction broadcast. +func MarkChannelCoopBroadcasted(backend kvdb.Backend, + channel *OpenChannel, closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error { + + return markBroadcasted( + backend, channel, ChanStatusCoopBroadcasted, coopCloseTxKey, + closeTx, closer, + ) +} + +// markBroadcasted modifies the channel status and inserts a close transaction +// under the requested key, which should specify either a coop or force close. +// It adds a status which indicates the party that initiated the channel close. +func markBroadcasted(backend kvdb.Backend, channel *OpenChannel, + status ChannelStatus, key []byte, closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error { + + if closeTx == nil { + return fmt.Errorf("closeTx must be non-nil") + } + + channel.Lock() + defer channel.Unlock() + + putClosingTx := func(chanBucket kvdb.RwBucket) error { + return PutChannelCloseTx(chanBucket, key, closeTx) + } + + // Add the initiator status to the status provided. These statuses are + // set in addition to the broadcast status so that we do not need to + // migrate the original logic which does not store initiator. + if closer.IsLocal() { + status |= ChanStatusLocalCloseInitiator + } else { + status |= ChanStatusRemoteCloseInitiator + } + + return PutChanStatus(backend, channel, status, putClosingTx) +} + +// FetchClosingTx returns the stored closing transaction for key. The caller +// should use either the force or coop closing keys. +func FetchClosingTx(backend kvdb.Backend, channel *OpenChannel, + key []byte) (*wire.MsgTx, error) { + + var closeTx *wire.MsgTx + + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + + return ErrNoCloseTx + default: + return err + } + + closeTx, err = FetchChannelCloseTx(chanBucket, key) + + return err + }, func() { + closeTx = nil + }) + if err != nil { + return nil, err + } + + return closeTx, nil +} From e9eb05092029b048162b42583919e868aedf33a0 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:34:23 -0300 Subject: [PATCH 39/61] chanstate: move shutdown store methods Move channel shutdown info store and fetch logic into the channel state KV shutdown file. Keep channeldb methods as wrappers while preserving the Store interface and shifting KV transaction handling into chanstate. --- channeldb/channel.go | 42 ++----------------------------- chanstate/kv_shutdown.go | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index b5db9ebe8a..fc1ad03ad4 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2,7 +2,6 @@ package channeldb import ( "bytes" - "errors" "fmt" "io" "net" @@ -405,17 +404,7 @@ var ( func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, info *ShutdownInfo) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - return cstate.PutChannelShutdownInfo(chanBucket, info) - }, func() {}) + return cstate.StoreChannelShutdownInfo(c.backend, channel, info) } // FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the target @@ -423,34 +412,7 @@ func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, func (c *ChannelStateDB) FetchChannelShutdownInfo( channel *OpenChannel) (fn.Option[ShutdownInfo], error) { - var shutdownInfo *ShutdownInfo - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - switch { - case err == nil: - case errors.Is(err, ErrNoChanDBExists), - errors.Is(err, ErrNoActiveChannels), - errors.Is(err, ErrChannelNotFound): - - return ErrNoShutdownInfo - default: - return err - } - - shutdownInfo, err = cstate.FetchChannelShutdownInfo(chanBucket) - - return err - }, func() { - shutdownInfo = nil - }) - if err != nil { - return fn.None[ShutdownInfo](), err - } - - return fn.Some[ShutdownInfo](*shutdownInfo), nil + return cstate.FetchShutdownInfo(c.backend, channel) } // isChannelBorked returns true if the channel has been marked as borked in the diff --git a/chanstate/kv_shutdown.go b/chanstate/kv_shutdown.go index 55ca0df908..46b2b8cb72 100644 --- a/chanstate/kv_shutdown.go +++ b/chanstate/kv_shutdown.go @@ -2,8 +2,10 @@ package chanstate import ( "bytes" + "errors" "io" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" @@ -50,6 +52,57 @@ func FetchChannelShutdownInfo(chanBucket kvdb.RBucket) ( return DecodeShutdownInfo(shutdownInfoBytes) } +// StoreChannelShutdownInfo persists the ShutdownInfo for the target channel. +func StoreChannelShutdownInfo(backend kvdb.Backend, channel *OpenChannel, + info *ShutdownInfo) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + return PutChannelShutdownInfo(chanBucket, info) + }, func() {}) +} + +// FetchShutdownInfo fetches the persisted ShutdownInfo for the target channel. +func FetchShutdownInfo(backend kvdb.Backend, + channel *OpenChannel) (fn.Option[ShutdownInfo], error) { + + var shutdownInfo *ShutdownInfo + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + + return ErrNoShutdownInfo + default: + return err + } + + shutdownInfo, err = FetchChannelShutdownInfo(chanBucket) + + return err + }, func() { + shutdownInfo = nil + }) + if err != nil { + return fn.None[ShutdownInfo](), err + } + + return fn.Some[ShutdownInfo](*shutdownInfo), nil +} + // EncodeShutdownInfo serialises the ShutdownInfo to the given io.Writer. func EncodeShutdownInfo(s *ShutdownInfo, w io.Writer) error { records := []tlv.Record{ From 2741c498e50ace0bed21702d67b2f188b74c3668 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:39:35 -0300 Subject: [PATCH 40/61] chanstate: move forwarding store methods Move forwarding package store methods into the channel state KV forwarding package file beside the packager implementation. Keep channeldb Store methods as wrappers so callers retain the same interface while forwarding KV transactions move into chanstate. --- channeldb/channel.go | 42 ++------------- chanstate/kv_forwarding_package.go | 84 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index fc1ad03ad4..c8ed58aa1d 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1125,19 +1125,7 @@ func putFinalHtlc(finalHtlcsBucket kvdb.RwBucket, id uint64, func (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, error) { - var fwdPkgs []*FwdPkg - if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - var err error - packager := NewChannelPackager(channel.ShortChannelID) - fwdPkgs, err = packager.LoadFwdPkgs(tx) - return err - }, func() { - fwdPkgs = nil - }); err != nil { - return nil, err - } - - return fwdPkgs, nil + return cstate.LoadFwdPkgs(c.backend, channel) } // AckAddHtlcs updates the AckAddFilter containing any of the provided AddRefs @@ -1146,10 +1134,7 @@ func (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, addRefs ...AddRef) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - packager := NewChannelPackager(channel.ShortChannelID) - return packager.AckAddHtlcs(tx, addRefs...) - }, func() {}) + return cstate.AckAddHtlcs(c.backend, channel, addRefs...) } // AckSettleFails updates the SettleFailFilter containing any of the provided @@ -1159,10 +1144,7 @@ func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, func (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, settleFailRefs ...SettleFailRef) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - packager := NewChannelPackager(channel.ShortChannelID) - return packager.AckSettleFails(tx, settleFailRefs...) - }, func() {}) + return cstate.AckSettleFails(c.backend, channel, settleFailRefs...) } // SetFwdFilter atomically sets the forwarding filter for the forwarding package @@ -1170,10 +1152,7 @@ func (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, fwdFilter *PkgFilter) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - packager := NewChannelPackager(channel.ShortChannelID) - return packager.SetFwdFilter(tx, height, fwdFilter) - }, func() {}) + return cstate.SetFwdFilter(c.backend, channel, height, fwdFilter) } // RemoveFwdPkgs atomically removes forwarding packages specified by the remote @@ -1184,18 +1163,7 @@ func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, heights ...uint64) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - packager := NewChannelPackager(channel.ShortChannelID) - - for _, height := range heights { - err := packager.RemovePkg(tx, height) - if err != nil { - return err - } - } - - return nil - }, func() {}) + return cstate.RemoveFwdPkgs(c.backend, channel, heights...) } // revocationLogTailCommitHeight returns the commit height at the end of the diff --git a/chanstate/kv_forwarding_package.go b/chanstate/kv_forwarding_package.go index 320e1a2167..93538b8ed7 100644 --- a/chanstate/kv_forwarding_package.go +++ b/chanstate/kv_forwarding_package.go @@ -254,6 +254,90 @@ func (p *ChannelPackager) Source() lnwire.ShortChannelID { return p.source } +func newChannelPackager(channel *OpenChannel) *ChannelPackager { + return NewChannelPackager(channel.ShortChannelID) +} + +// LoadFwdPkgs scans the forwarding log for any packages that haven't been +// processed, and returns their deserialized log updates in map indexed by the +// remote commitment height at which the updates were locked in. +func LoadFwdPkgs(backend kvdb.Backend, channel *OpenChannel) ([]*FwdPkg, + error) { + + var fwdPkgs []*FwdPkg + if err := kvdb.View(backend, func(tx kvdb.RTx) error { + var err error + fwdPkgs, err = newChannelPackager(channel).LoadFwdPkgs(tx) + return err + }, func() { + fwdPkgs = nil + }); err != nil { + return nil, err + } + + return fwdPkgs, nil +} + +// AckAddHtlcs updates the AckAddFilter containing any of the provided AddRefs +// indicating that a response to this Add has been committed to the remote +// party. Doing so will prevent these Add HTLCs from being reforwarded +// internally. +func AckAddHtlcs(backend kvdb.Backend, channel *OpenChannel, + addRefs ...AddRef) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return newChannelPackager(channel).AckAddHtlcs(tx, addRefs...) + }, func() {}) +} + +// AckSettleFails updates the SettleFailFilter containing any of the provided +// SettleFailRefs, indicating that the response has been delivered to the +// incoming link, corresponding to a particular AddRef. Doing so will prevent +// the responses from being retransmitted internally. +func AckSettleFails(backend kvdb.Backend, channel *OpenChannel, + settleFailRefs ...SettleFailRef) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return newChannelPackager(channel).AckSettleFails( + tx, settleFailRefs..., + ) + }, func() {}) +} + +// SetFwdFilter atomically sets the forwarding filter for the forwarding package +// identified by `height`. +func SetFwdFilter(backend kvdb.Backend, channel *OpenChannel, height uint64, + fwdFilter *PkgFilter) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return newChannelPackager(channel).SetFwdFilter( + tx, height, fwdFilter, + ) + }, func() {}) +} + +// RemoveFwdPkgs atomically removes forwarding packages specified by the remote +// commitment heights. If one of the intermediate RemovePkg calls fails, then +// the later packages won't be removed. +// +// NOTE: This method should only be called on packages marked FwdStateCompleted. +func RemoveFwdPkgs(backend kvdb.Backend, channel *OpenChannel, + heights ...uint64) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + packager := newChannelPackager(channel) + + for _, height := range heights { + err := packager.RemovePkg(tx, height) + if err != nil { + return err + } + } + + return nil + }, func() {}) +} + // AddFwdPkg writes a newly locked in forwarding package to disk. func (*ChannelPackager) AddFwdPkg(tx kvdb.RwTx, fwdPkg *FwdPkg) error { fwdPkgBkt, err := tx.CreateTopLevelBucket(fwdPackagesKey) From b9a9f28d854b44bc4d0abc0e90412b7b6ba02a6b Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:44:47 -0300 Subject: [PATCH 41/61] chanstate: move commitment read methods Move remote commitment tip, unsigned update reads, and next revocation persistence into the channel state KV commitment file. Keep channeldb methods as wrappers while commitment transaction logic continues moving into chanstate. --- channeldb/channel.go | 119 ++-------------------------- chanstate/kv_commitment.go | 154 +++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 114 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index c8ed58aa1d..d7056b6eb7 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -802,6 +802,7 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, if err := cstate.SerializeCommitDiff(&b2, diff); err != nil { return err } + return chanBucket.Put(commitDiffKey, b2.Bytes()) }, func() {}) } @@ -811,41 +812,7 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( *CommitDiff, error) { - var cd *CommitDiff - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - switch err { - case nil: - case ErrNoChanDBExists, ErrNoActiveChannels, ErrChannelNotFound: - return ErrNoPendingCommit - default: - return err - } - - tipBytes := chanBucket.Get(commitDiffKey) - if tipBytes == nil { - return ErrNoPendingCommit - } - - tipReader := bytes.NewReader(tipBytes) - dcd, err := cstate.DeserializeCommitDiff(tipReader) - if err != nil { - return err - } - - cd = dcd - return nil - }, func() { - cd = nil - }) - if err != nil { - return nil, err - } - - return cd, nil + return cstate.RemoteCommitChainTip(c.backend, channel) } // UnsignedAckedUpdates retrieves the persisted unsigned acked remote log @@ -853,36 +820,7 @@ func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( []LogUpdate, error) { - var updates []LogUpdate - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - switch err { - case nil: - case ErrNoChanDBExists, ErrNoActiveChannels, ErrChannelNotFound: - return nil - default: - return err - } - - updateBytes := chanBucket.Get(unsignedAckedUpdatesKey) - if updateBytes == nil { - return nil - } - - r := bytes.NewReader(updateBytes) - updates, err = cstate.DeserializeLogUpdates(r) - return err - }, func() { - updates = nil - }) - if err != nil { - return nil, err - } - - return updates, nil + return cstate.UnsignedAckedUpdates(c.backend, channel) } // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log @@ -890,37 +828,7 @@ func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( []LogUpdate, error) { - var updates []LogUpdate - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - switch err { - case nil: - break - case ErrNoChanDBExists, ErrNoActiveChannels, ErrChannelNotFound: - return nil - default: - return err - } - - updateBytes := chanBucket.Get(remoteUnsignedLocalUpdatesKey) - if updateBytes == nil { - return nil - } - - r := bytes.NewReader(updateBytes) - updates, err = cstate.DeserializeLogUpdates(r) - return err - }, func() { - updates = nil - }) - if err != nil { - return nil, err - } - - return updates, nil + return cstate.RemoteUnsignedLocalUpdates(c.backend, channel) } // InsertNextRevocation inserts the next commitment point into the persisted @@ -928,24 +836,7 @@ func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( func (c *ChannelStateDB) InsertNextRevocation(channel *OpenChannel, revKey *btcec.PublicKey) error { - channel.RemoteNextRevocation = revKey - - err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - return putChanRevocationState(chanBucket, channel) - }, func() {}) - if err != nil { - return err - } - - return nil + return cstate.InsertNextRevocation(c.backend, channel, revKey) } // AdvanceCommitChainTail records the new state transition within the diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go index c14abb0813..22d66ae749 100644 --- a/chanstate/kv_commitment.go +++ b/chanstate/kv_commitment.go @@ -3,6 +3,7 @@ package chanstate import ( "bytes" "encoding/binary" + "errors" "fmt" "io" @@ -417,6 +418,159 @@ func DeleteOpenChannel(chanBucket kvdb.RwBucket) error { return nil } +// RemoteCommitChainTip returns the "tip" of the current remote commitment +// chain. +func RemoteCommitChainTip(backend kvdb.Backend, + channel *OpenChannel) (*CommitDiff, error) { + + var cd *CommitDiff + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + + return ErrNoPendingCommit + default: + return err + } + + tipBytes := chanBucket.Get(commitDiffKey) + if tipBytes == nil { + return ErrNoPendingCommit + } + + tipReader := bytes.NewReader(tipBytes) + dcd, err := DeserializeCommitDiff(tipReader) + if err != nil { + return err + } + + cd = dcd + + return nil + }, func() { + cd = nil + }) + if err != nil { + return nil, err + } + + return cd, nil +} + +// UnsignedAckedUpdates retrieves the persisted unsigned acked remote log +// updates that still need to be signed for. +func UnsignedAckedUpdates(backend kvdb.Backend, + channel *OpenChannel) ([]LogUpdate, error) { + + var updates []LogUpdate + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + + return nil + default: + return err + } + + updateBytes := chanBucket.Get(unsignedAckedUpdatesKey) + if updateBytes == nil { + return nil + } + + r := bytes.NewReader(updateBytes) + updates, err = DeserializeLogUpdates(r) + + return err + }, func() { + updates = nil + }) + if err != nil { + return nil, err + } + + return updates, nil +} + +// RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log +// updates that the remote still needs to sign for. +func RemoteUnsignedLocalUpdates(backend kvdb.Backend, + channel *OpenChannel) ([]LogUpdate, error) { + + var updates []LogUpdate + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + switch { + case err == nil: + case errors.Is(err, ErrNoChanDBExists), + errors.Is(err, ErrNoActiveChannels), + errors.Is(err, ErrChannelNotFound): + + return nil + default: + return err + } + + updateBytes := chanBucket.Get(remoteUnsignedLocalUpdatesKey) + if updateBytes == nil { + return nil + } + + r := bytes.NewReader(updateBytes) + updates, err = DeserializeLogUpdates(r) + + return err + }, func() { + updates = nil + }) + if err != nil { + return nil, err + } + + return updates, nil +} + +// InsertNextRevocation inserts the next commitment point into the persisted +// channel state. +func InsertNextRevocation(backend kvdb.Backend, channel *OpenChannel, + revKey *btcec.PublicKey) error { + + channel.RemoteNextRevocation = revKey + + err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + return PutChanRevocationState(chanBucket, channel) + }, func() {}) + if err != nil { + return err + } + + return nil +} + // commitTlvData stores all the optional data that may be stored as a TLV stream // at the _end_ of the normal serialized commit on disk. type commitTlvData struct { From 9a2775de0c63de6c1d88a6f0c3843fecfd570908 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 08:57:58 -0300 Subject: [PATCH 42/61] chanstate: move commit chain kv store Move the commit-chain mutation and read paths into the chanstate KV store implementation. Keep channeldb as a thin wrapper while callers still use the existing ChannelStateDB surface. --- channeldb/channel.go | 290 ++------------------------------- chanstate/kv_commitment.go | 325 +++++++++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+), 278 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index d7056b6eb7..98de94907a 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -40,7 +40,6 @@ var ( historicalChannelBucket = cstate.HistoricalChannelBucketKey() unsignedAckedUpdatesKey = cstate.UnsignedAckedUpdatesKey() remoteUnsignedLocalUpdatesKey = cstate.RemoteUnsignedLocalUpdatesKey() - commitDiffKey = cstate.CommitDiffKey() lastWasRevokeKey = cstate.LastWasRevokeKey() ) @@ -602,6 +601,7 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, "updates: %v", err) } + //nolint:ll // Since we have just sent the counterparty a revocation, store true // under lastWasRevokeKey. var b2 bytes.Buffer @@ -609,10 +609,12 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, return err } - if err := chanBucket.Put(lastWasRevokeKey, b2.Bytes()); err != nil { + err = chanBucket.Put(lastWasRevokeKey, b2.Bytes()) + if err != nil { return err } + //nolint:ll // Persist the remote unsigned local updates that are not included // in our new commitment. updateBytes := chanBucket.Get(remoteUnsignedLocalUpdatesKey) @@ -739,72 +741,7 @@ func newChannelPackager(channel *OpenChannel) *ChannelPackager { func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, diff *CommitDiff) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - // First, we'll grab the writable bucket where this channel's - // data resides. - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - // If the channel is marked as borked, then for safety reasons, - // we shouldn't attempt any further updates. - isBorked, err := isChannelBorked(channel, chanBucket) - if err != nil { - return err - } - if isBorked { - return ErrChanBorked - } - - // Any outgoing settles and fails necessarily have a - // corresponding adds in this channel's forwarding packages. - // Mark all of these as being fully processed in our forwarding - // package, which prevents us from reprocessing them after - // startup. - packager := NewChannelPackager(channel.ShortChannelID) - - err = packager.AckAddHtlcs(tx, diff.AddAcks...) - if err != nil { - return err - } - - // Additionally, we ack from any fails or settles that are - // persisted in another channel's forwarding package. This - // prevents the same fails and settles from being retransmitted - // after restarts. The actual fail or settle we need to - // propagate to the remote party is now in the commit diff. - err = packager.AckSettleFails( - tx, diff.SettleFailAcks..., - ) - if err != nil { - return err - } - - // We are sending a commitment signature so lastWasRevokeKey should - // store false. - var b bytes.Buffer - if err := WriteElements(&b, false); err != nil { - return err - } - if err := chanBucket.Put(lastWasRevokeKey, b.Bytes()); err != nil { - return err - } - - // TODO(roasbeef): use seqno to derive key for later LCP - - // With the bucket retrieved, we'll now serialize the commit - // diff itself, and write it to disk. - var b2 bytes.Buffer - if err := cstate.SerializeCommitDiff(&b2, diff); err != nil { - return err - } - - return chanBucket.Put(commitDiffKey, b2.Bytes()) - }, func() {}) + return cstate.AppendRemoteCommitChain(c.backend, channel, diff) } // RemoteCommitChainTip returns the "tip" of the current remote commitment @@ -846,157 +783,10 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, fwdPkg *FwdPkg, updates []LogUpdate, ourOutputIndex, theirOutputIndex uint32) error { - var newRemoteCommit *ChannelCommitment - - err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - // If the channel is marked as borked, then for safety reasons, - // we shouldn't attempt any further updates. - isBorked, err := isChannelBorked(channel, chanBucket) - if err != nil { - return err - } - if isBorked { - return ErrChanBorked - } - - // Persist the latest preimage state to disk as the remote peer - // has just added to our local preimage store, and given us a - // new pending revocation key. - err = putChanRevocationState(chanBucket, channel) - if err != nil { - return err - } - - // With the current preimage producer/store state updated, - // append a new log entry recording this the delta of this - // state transition. - // - // TODO(roasbeef): could make the deltas relative, would save - // space, but then tradeoff for more disk-seeks to recover the - // full state. - logKey := revocationLogBucket - logBucket, err := chanBucket.CreateBucketIfNotExists(logKey) - if err != nil { - return err - } - - // Before we append this revoked state to the revocation log, - // we'll swap out what's currently the tail of the commit tip, - // with the current locked-in commitment for the remote party. - tipBytes := chanBucket.Get(commitDiffKey) - tipReader := bytes.NewReader(tipBytes) - newCommit, err := cstate.DeserializeCommitDiff(tipReader) - if err != nil { - return err - } - err = putChanCommitment( - chanBucket, &newCommit.Commitment, false, - ) - if err != nil { - return err - } - if err := chanBucket.Delete(commitDiffKey); err != nil { - return err - } - - // With the commitment pointer swapped, we can now add the - // revoked (prior) state to the revocation log. - err = putRevocationLog( - logBucket, &channel.RemoteCommitment, ourOutputIndex, - theirOutputIndex, c.parent.noRevLogAmtData, - ) - if err != nil { - return err - } - - // Lastly, we write the forwarding package to disk so that we - // can properly recover from failures and reforward HTLCs that - // have not received a corresponding settle/fail. - packager := NewChannelPackager(channel.ShortChannelID) - err = packager.AddFwdPkg(tx, fwdPkg) - if err != nil { - return err - } - - // Persist the unsigned acked updates that are not included - // in their new commitment. - updateBytes := chanBucket.Get(unsignedAckedUpdatesKey) - if updateBytes == nil { - // This shouldn't normally happen as we always store - // the number of updates, but could still be - // encountered by nodes that are upgrading. - newRemoteCommit = &newCommit.Commitment - return nil - } - - r := bytes.NewReader(updateBytes) - unsignedUpdates, err := cstate.DeserializeLogUpdates(r) - if err != nil { - return err - } - - var validUpdates []LogUpdate - for _, upd := range unsignedUpdates { - lIdx := upd.LogIndex - - // Filter for updates that are not on the remote - // commitment. - if lIdx >= newCommit.Commitment.RemoteLogIndex { - validUpdates = append(validUpdates, upd) - } - } - - var b bytes.Buffer - err = cstate.SerializeLogUpdates(&b, validUpdates) - if err != nil { - return fmt.Errorf("unable to serialize log updates: %w", - err) - } - - err = chanBucket.Put(unsignedAckedUpdatesKey, b.Bytes()) - if err != nil { - return fmt.Errorf("unable to store under "+ - "unsignedAckedUpdatesKey: %w", err) - } - - // Persist the local updates the peer hasn't yet signed so they - // can be restored after restart. - var b2 bytes.Buffer - err = cstate.SerializeLogUpdates(&b2, updates) - if err != nil { - return err - } - - err = chanBucket.Put(remoteUnsignedLocalUpdatesKey, b2.Bytes()) - if err != nil { - return fmt.Errorf("unable to restore remote unsigned "+ - "local updates: %v", err) - } - - newRemoteCommit = &newCommit.Commitment - - return nil - }, func() { - newRemoteCommit = nil - }) - if err != nil { - return err - } - - // With the db transaction complete, we'll swap over the in-memory - // pointer of the new remote commitment, which was previously the tip - // of the commit chain. - channel.RemoteCommitment = *newRemoteCommit - - return nil + return cstate.AdvanceCommitChainTail( + c.backend, channel, fwdPkg, updates, ourOutputIndex, + theirOutputIndex, c.parent.noRevLogAmtData, + ) } // FinalHtlcInfo contains information about the final outcome of an htlc. @@ -1108,33 +898,7 @@ func (c *ChannelStateDB) revocationLogTailCommitHeight( func (c *ChannelStateDB) CommitmentHeight(channel *OpenChannel) ( uint64, error) { - var height uint64 - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - // Get the bucket dedicated to storing the metadata for open - // channels. - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - commit, err := fetchChanCommitment(chanBucket, true) - if err != nil { - return err - } - - height = commit.CommitHeight - return nil - }, func() { - height = 0 - }) - if err != nil { - return 0, err - } - - return height, nil + return cstate.CommitmentHeight(c.backend, channel) } // FindPreviousState scans through the append-only log in an attempt to recover @@ -1423,22 +1187,7 @@ type ChannelSnapshot = cstate.ChannelSnapshot func (c *ChannelStateDB) LatestCommitments(channel *OpenChannel) ( *ChannelCommitment, *ChannelCommitment, error) { - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - return fetchChanCommitments(chanBucket, channel) - }, func() {}) - if err != nil { - return nil, nil, err - } - - return &channel.LocalCommitment, &channel.RemoteCommitment, nil + return cstate.LatestCommitments(c.backend, channel) } // RemoteRevocationStore returns the most up to date commitment version of the @@ -1448,22 +1197,7 @@ func (c *ChannelStateDB) LatestCommitments(channel *OpenChannel) ( func (c *ChannelStateDB) RemoteRevocationStore(channel *OpenChannel) ( shachain.Store, error) { - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - return fetchChanRevocationState(chanBucket, channel) - }, func() {}) - if err != nil { - return nil, err - } - - return channel.RevocationStore, nil + return cstate.RemoteRevocationStore(c.backend, channel) } func putChannelCloseSummary(tx kvdb.RwTx, chanID []byte, diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go index 22d66ae749..556791331f 100644 --- a/chanstate/kv_commitment.go +++ b/chanstate/kv_commitment.go @@ -12,6 +12,7 @@ import ( "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" "github.com/lightningnetwork/lnd/tlv" ) @@ -571,6 +572,330 @@ func InsertNextRevocation(backend kvdb.Backend, channel *OpenChannel, return nil } +// AppendRemoteCommitChain appends a new CommitDiff to the remote party's +// commitment chain. +func AppendRemoteCommitChain(backend kvdb.Backend, channel *OpenChannel, + diff *CommitDiff) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + // First, we'll grab the writable bucket where this channel's + // data resides. + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + // If the channel is marked as borked, then for safety reasons, + // we shouldn't attempt any further updates. + isBorked, err := IsChannelBorked(channel, chanBucket) + if err != nil { + return err + } + if isBorked { + return ErrChanBorked + } + + // Any outgoing settles and fails necessarily have a + // corresponding adds in this channel's forwarding packages. + // Mark all of these as being fully processed in our forwarding + // package, which prevents us from reprocessing them after + // startup. + packager := NewChannelPackager(channel.ShortChannelID) + + err = packager.AckAddHtlcs(tx, diff.AddAcks...) + if err != nil { + return err + } + + // Additionally, we ack from any fails or settles that are + // persisted in another channel's forwarding package. This + // prevents the same fails and settles from being retransmitted + // after restarts. The actual fail or settle we need to + // propagate to the remote party is now in the commit diff. + err = packager.AckSettleFails( + tx, diff.SettleFailAcks..., + ) + if err != nil { + return err + } + + //nolint:ll + // We are sending a commitment signature so lastWasRevokeKey should + // store false. + var b bytes.Buffer + if err := WriteElements(&b, false); err != nil { + return err + } + err = chanBucket.Put(lastWasRevokeKey, b.Bytes()) + if err != nil { + return err + } + + // TODO(roasbeef): use seqno to derive key for later LCP + + // With the bucket retrieved, we'll now serialize the commit + // diff itself, and write it to disk. + var b2 bytes.Buffer + if err := SerializeCommitDiff(&b2, diff); err != nil { + return err + } + + return chanBucket.Put(commitDiffKey, b2.Bytes()) + }, func() {}) +} + +// AdvanceCommitChainTail records the new state transition within the +// revocation log and promotes the pending remote commitment to the current +// remote commitment. +func AdvanceCommitChainTail(backend kvdb.Backend, channel *OpenChannel, + fwdPkg *FwdPkg, updates []LogUpdate, ourOutputIndex, + theirOutputIndex uint32, noRevLogAmtData bool) error { + + var newRemoteCommit *ChannelCommitment + + err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + // If the channel is marked as borked, then for safety reasons, + // we shouldn't attempt any further updates. + isBorked, err := IsChannelBorked(channel, chanBucket) + if err != nil { + return err + } + if isBorked { + return ErrChanBorked + } + + // Persist the latest preimage state to disk as the remote peer + // has just added to our local preimage store, and given us a + // new pending revocation key. + err = PutChanRevocationState(chanBucket, channel) + if err != nil { + return err + } + + // With the current preimage producer/store state updated, + // append a new log entry recording this the delta of this + // state transition. + // + // TODO(roasbeef): could make the deltas relative, would save + // space, but then tradeoff for more disk-seeks to recover the + // full state. + logKey := revocationLogBucket + logBucket, err := chanBucket.CreateBucketIfNotExists(logKey) + if err != nil { + return err + } + + // Before we append this revoked state to the revocation log, + // we'll swap out what's currently the tail of the commit tip, + // with the current locked-in commitment for the remote party. + tipBytes := chanBucket.Get(commitDiffKey) + tipReader := bytes.NewReader(tipBytes) + newCommit, err := DeserializeCommitDiff(tipReader) + if err != nil { + return err + } + err = PutChanCommitment( + chanBucket, &newCommit.Commitment, false, + ) + if err != nil { + return err + } + if err := chanBucket.Delete(commitDiffKey); err != nil { + return err + } + + // With the commitment pointer swapped, we can now add the + // revoked (prior) state to the revocation log. + err = PutRevocationLog( + logBucket, &channel.RemoteCommitment, ourOutputIndex, + theirOutputIndex, noRevLogAmtData, + ) + if err != nil { + return err + } + + // Lastly, we write the forwarding package to disk so that we + // can properly recover from failures and reforward HTLCs that + // have not received a corresponding settle/fail. + err = NewChannelPackager(channel.ShortChannelID).AddFwdPkg( + tx, fwdPkg, + ) + if err != nil { + return err + } + + // Persist the unsigned acked updates that are not included + // in their new commitment. + updateBytes := chanBucket.Get(unsignedAckedUpdatesKey) + if updateBytes == nil { + // This shouldn't normally happen as we always store + // the number of updates, but could still be + // encountered by nodes that are upgrading. + newRemoteCommit = &newCommit.Commitment + return nil + } + + r := bytes.NewReader(updateBytes) + unsignedUpdates, err := DeserializeLogUpdates(r) + if err != nil { + return err + } + + var validUpdates []LogUpdate + for _, upd := range unsignedUpdates { + lIdx := upd.LogIndex + + // Filter for updates that are not on the remote + // commitment. + if lIdx >= newCommit.Commitment.RemoteLogIndex { + validUpdates = append(validUpdates, upd) + } + } + + var b bytes.Buffer + err = SerializeLogUpdates(&b, validUpdates) + if err != nil { + return fmt.Errorf("unable to serialize log updates: %w", + err) + } + + err = chanBucket.Put(unsignedAckedUpdatesKey, b.Bytes()) + if err != nil { + return fmt.Errorf("unable to store under "+ + "unsignedAckedUpdatesKey: %w", err) + } + + // Persist the local updates the peer hasn't yet signed so they + // can be restored after restart. + var b2 bytes.Buffer + err = SerializeLogUpdates(&b2, updates) + if err != nil { + return err + } + + err = chanBucket.Put(remoteUnsignedLocalUpdatesKey, b2.Bytes()) + if err != nil { + return fmt.Errorf("unable to restore remote unsigned "+ + "local updates: %v", err) + } + + newRemoteCommit = &newCommit.Commitment + + return nil + }, func() { + newRemoteCommit = nil + }) + if err != nil { + return err + } + + // With the db transaction complete, we'll swap over the in-memory + // pointer of the new remote commitment, which was previously the tip + // of the commit chain. + channel.RemoteCommitment = *newRemoteCommit + + return nil +} + +// CommitmentHeight returns the current commitment height. The commitment +// height represents the number of updates to the commitment state to date. +// This value is always monotonically increasing. This method is provided in +// order to allow multiple instances of a particular open channel to obtain a +// consistent view of the number of channel updates to date. +func CommitmentHeight(backend kvdb.Backend, channel *OpenChannel) ( + uint64, error) { + + var height uint64 + err := kvdb.View(backend, func(tx kvdb.RTx) error { + // Get the bucket dedicated to storing the metadata for open + // channels. + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + commit, err := FetchChanCommitment(chanBucket, true) + if err != nil { + return err + } + + height = commit.CommitHeight + + return nil + }, func() { + height = 0 + }) + if err != nil { + return 0, err + } + + return height, nil +} + +// LatestCommitments returns the two latest commitments for both the local and +// remote party. These commitments are read from disk to ensure that only the +// latest fully committed state is returned. The first commitment returned is +// the local commitment, and the second returned is the remote commitment. +func LatestCommitments(backend kvdb.Backend, channel *OpenChannel) ( + *ChannelCommitment, *ChannelCommitment, error) { + + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + return FetchChanCommitments(chanBucket, channel) + }, func() {}) + if err != nil { + return nil, nil, err + } + + return &channel.LocalCommitment, &channel.RemoteCommitment, nil +} + +// RemoteRevocationStore returns the most up to date commitment version of the +// revocation storage tree for the remote party. This method can be used when +// acting on a possible contract breach to ensure, that the caller has the most +// up to date information required to deliver justice. +func RemoteRevocationStore(backend kvdb.Backend, + channel *OpenChannel) (shachain.Store, error) { + + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + return FetchChanRevocationState(chanBucket, channel) + }, func() {}) + if err != nil { + return nil, err + } + + return channel.RevocationStore, nil +} + // commitTlvData stores all the optional data that may be stored as a TLV stream // at the _end_ of the normal serialized commit on disk. type commitTlvData struct { From d00deda4d8763f6fec8a78b510fc963d827e49ac Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 09:08:22 -0300 Subject: [PATCH 43/61] chanstate: move revocation log kv store Move revocation-log lookup compatibility and tail-height queries into the chanstate KV store implementation. Keep channeldb wrappers for existing tests and callers while the package boundary is being moved. --- channeldb/channel.go | 68 +---------- channeldb/error.go | 2 +- channeldb/revocation_log.go | 89 +------------- chanstate/errors.go | 4 + chanstate/kv_revocation_log.go | 213 +++++++++++++++++++++++++++++++++ 5 files changed, 226 insertions(+), 150 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 98de94907a..25324c45ce 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -852,42 +852,7 @@ func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, func (c *ChannelStateDB) revocationLogTailCommitHeight( channel *OpenChannel) (uint64, error) { - var height uint64 - - // If we haven't created any state updates yet, then we'll exit early as - // there's nothing to be found on disk in the revocation bucket. - if channel.RemoteCommitment.CommitHeight == 0 { - return height, nil - } - - if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - logBucket, err := fetchLogBucket(chanBucket) - if err != nil { - return err - } - - // Once we have the bucket that stores the revocation log from - // this channel, we'll jump to the _last_ key in bucket. Since - // the key is the commit height, we'll decode the bytes and - // return it. - cursor := logBucket.ReadCursor() - rawHeight, _ := cursor.Last() - height = byteOrder.Uint64(rawHeight) - - return nil - }, func() {}); err != nil { - return height, err - } - - return height, nil + return cstate.RevocationLogTailCommitHeight(c.backend, channel) } // CommitmentHeight returns the current commitment height. The commitment @@ -909,36 +874,7 @@ func (c *ChannelStateDB) CommitmentHeight(channel *OpenChannel) ( func (c *ChannelStateDB) FindPreviousState(channel *OpenChannel, updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { - commit := &ChannelCommitment{} - rl := &RevocationLog{} - - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - // Find the revocation log from both the new and the old - // bucket. - r, c, err := fetchRevocationLogCompatible(chanBucket, updateNum) - if err != nil { - return err - } - - rl = r - commit = c - return nil - }, func() {}) - if err != nil { - return nil, nil, err - } - - // Either the `rl` or the `commit` is nil here. We return them as-is - // and leave it to the caller to decide its following action. - return rl, commit, nil + return cstate.FindPreviousState(c.backend, channel, updateNum) } // ClosureType is an enum like structure that details exactly how a channel was diff --git a/channeldb/error.go b/channeldb/error.go index ce1949f66c..ad50964a01 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -30,7 +30,7 @@ var ( // ErrNoPastDeltas is returned when the channel delta bucket hasn't been // created. - ErrNoPastDeltas = fmt.Errorf("channel has no recorded deltas") + ErrNoPastDeltas = cstate.ErrNoPastDeltas // ErrNodeNotFound is returned when node bucket exists, but node with // specific identity can't be found. diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 988ae1f25d..59fcf68734 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -1,7 +1,6 @@ package channeldb import ( - "bytes" "io" cstate "github.com/lightningnetwork/lnd/chanstate" @@ -56,7 +55,8 @@ var ( // // Deprecated: This bucket is kept for read-only in case the user // choose not to migrate the old data. - revocationLogBucketDeprecated = []byte("revocation-log-key") + //nolint:ll + revocationLogBucketDeprecated = cstate.RevocationLogBucketDeprecatedKey() // revocationLogBucket is a sub-bucket under openChannelBucket. This // sub-bucket is dedicated for storing the minimal info required to @@ -133,14 +133,7 @@ func readTlvStream(r io.Reader, s *tlv.Stream) (tlv.TypeMap, error) { func fetchOldRevocationLog(log kvdb.RBucket, updateNum uint64) (ChannelCommitment, error) { - logEntrykey := makeLogKey(updateNum) - commitBytes := log.Get(logEntrykey[:]) - if commitBytes == nil { - return ChannelCommitment{}, ErrLogEntryNotFound - } - - commitReader := bytes.NewReader(commitBytes) - return deserializeChanCommit(commitReader) + return cstate.FetchOldRevocationLog(log, updateNum) } // fetchRevocationLogCompatible finds the revocation log from both the @@ -154,86 +147,16 @@ func fetchOldRevocationLog(log kvdb.RBucket, func fetchRevocationLogCompatible(chanBucket kvdb.RBucket, updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { - // Look into the new bucket first. - logBucket := chanBucket.NestedReadBucket(revocationLogBucket) - if logBucket != nil { - rl, err := fetchRevocationLog(logBucket, updateNum) - // We've found the record, no need to visit the old bucket. - if err == nil { - return &rl, nil, nil - } - - // Return the error if it doesn't say the log cannot be found. - if err != ErrLogEntryNotFound { - return nil, nil, err - } - } - - // Otherwise, look into the old bucket and try to find the log there. - oldBucket := chanBucket.NestedReadBucket(revocationLogBucketDeprecated) - if oldBucket != nil { - c, err := fetchOldRevocationLog(oldBucket, updateNum) - if err != nil { - return nil, nil, err - } - - // Found an old record and return it. - return nil, &c, nil - } - - // If both the buckets are nil, then the sub-buckets haven't been - // created yet. - if logBucket == nil && oldBucket == nil { - return nil, nil, ErrNoPastDeltas - } - - // Otherwise, we've tried to query the new bucket but the log cannot be - // found. - return nil, nil, ErrLogEntryNotFound + return cstate.FetchRevocationLogCompatible(chanBucket, updateNum) } // fetchLogBucket returns a read bucket by visiting both the new and the old // bucket. func fetchLogBucket(chanBucket kvdb.RBucket) (kvdb.RBucket, error) { - logBucket := chanBucket.NestedReadBucket(revocationLogBucket) - if logBucket == nil { - logBucket = chanBucket.NestedReadBucket( - revocationLogBucketDeprecated, - ) - if logBucket == nil { - return nil, ErrNoPastDeltas - } - } - - return logBucket, nil + return cstate.FetchLogBucket(chanBucket) } // deleteLogBucket deletes the both the new and old revocation log buckets. func deleteLogBucket(chanBucket kvdb.RwBucket) error { - // Check if the bucket exists and delete it. - logBucket := chanBucket.NestedReadWriteBucket( - revocationLogBucket, - ) - if logBucket != nil { - err := chanBucket.DeleteNestedBucket(revocationLogBucket) - if err != nil { - return err - } - } - - // We also check whether the old revocation log bucket exists - // and delete it if so. - oldLogBucket := chanBucket.NestedReadWriteBucket( - revocationLogBucketDeprecated, - ) - if oldLogBucket != nil { - err := chanBucket.DeleteNestedBucket( - revocationLogBucketDeprecated, - ) - if err != nil { - return err - } - } - - return nil + return cstate.DeleteLogBucket(chanBucket) } diff --git a/chanstate/errors.go b/chanstate/errors.go index fe6ffce346..b33d05cfe5 100644 --- a/chanstate/errors.go +++ b/chanstate/errors.go @@ -41,6 +41,10 @@ var ( // channels within the database. ErrNoActiveChannels = fmt.Errorf("no active channels exist") + // ErrNoPastDeltas is returned when the channel delta bucket hasn't + // been created. + ErrNoPastDeltas = fmt.Errorf("channel has no recorded deltas") + // ErrNoCommitPoint is returned when no data loss commit point is found // in the database. ErrNoCommitPoint = fmt.Errorf("no commit point found") diff --git a/chanstate/kv_revocation_log.go b/chanstate/kv_revocation_log.go index eb532d527f..c2e84869ed 100644 --- a/chanstate/kv_revocation_log.go +++ b/chanstate/kv_revocation_log.go @@ -13,6 +13,16 @@ import ( ) var ( + // revocationLogBucketDeprecated is dedicated for storing the necessary + // delta state between channel updates required to re-construct a past + // state in order to punish a counterparty attempting a non-cooperative + // channel closure. This key should be accessed from within the + // sub-bucket of a target channel, identified by its channel point. + // + // Deprecated: This bucket is kept for read-only in case the user + // choose not to migrate the old data. + revocationLogBucketDeprecated = []byte("revocation-log-key") + // revocationLogBucket is a sub-bucket under openChannelBucket. This // sub-bucket is dedicated for storing the minimal info required to // re-construct a past state in order to punish a counterparty @@ -34,6 +44,12 @@ func RevocationLogBucketKey() []byte { return revocationLogBucket } +// RevocationLogBucketDeprecatedKey returns the deprecated revocation-log bucket +// key. +func RevocationLogBucketDeprecatedKey() []byte { + return revocationLogBucketDeprecated +} + // PutRevocationLog uses the fields `CommitTx` and `Htlcs` from a // ChannelCommitment to construct a revocation log entry and saves them to // disk. It also saves our output index and their output index, which are @@ -124,6 +140,203 @@ func FetchRevocationLog(log kvdb.RBucket, return DeserializeRevocationLog(commitReader) } +// FetchOldRevocationLog finds the revocation log from the deprecated +// sub-bucket. +func FetchOldRevocationLog(log kvdb.RBucket, + updateNum uint64) (ChannelCommitment, error) { + + logEntrykey := revocationLogKey(updateNum) + commitBytes := log.Get(logEntrykey[:]) + if commitBytes == nil { + return ChannelCommitment{}, ErrLogEntryNotFound + } + + commitReader := bytes.NewReader(commitBytes) + + return DeserializeChanCommit(commitReader) +} + +// FetchRevocationLogCompatible finds the revocation log from both the +// revocationLogBucket and revocationLogBucketDeprecated for compatibility +// concern. It returns three values, +// - RevocationLog, if this is non-nil, it means we've found the log in the +// new bucket. +// - ChannelCommitment, if this is non-nil, it means we've found the log +// in the old bucket. +// - error, this can happen if the log cannot be found in neither buckets. +func FetchRevocationLogCompatible(chanBucket kvdb.RBucket, + updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { + + // Look into the new bucket first. + logBucket := chanBucket.NestedReadBucket(revocationLogBucket) + if logBucket != nil { + rl, err := FetchRevocationLog(logBucket, updateNum) + // We've found the record, no need to visit the old bucket. + if err == nil { + return &rl, nil, nil + } + + // Return the error if it doesn't say the log cannot be found. + if !errors.Is(err, ErrLogEntryNotFound) { + return nil, nil, err + } + } + + // Otherwise, look into the old bucket and try to find the log there. + oldBucket := chanBucket.NestedReadBucket(revocationLogBucketDeprecated) + if oldBucket != nil { + c, err := FetchOldRevocationLog(oldBucket, updateNum) + if err != nil { + return nil, nil, err + } + + // Found an old record and return it. + return nil, &c, nil + } + + // If both the buckets are nil, then the sub-buckets haven't been + // created yet. + if logBucket == nil && oldBucket == nil { + return nil, nil, ErrNoPastDeltas + } + + // Otherwise, we've tried to query the new bucket but the log cannot be + // found. + return nil, nil, ErrLogEntryNotFound +} + +// FetchLogBucket returns a read bucket by visiting both the new and the old +// bucket. +func FetchLogBucket(chanBucket kvdb.RBucket) (kvdb.RBucket, error) { + logBucket := chanBucket.NestedReadBucket(revocationLogBucket) + if logBucket == nil { + logBucket = chanBucket.NestedReadBucket( + revocationLogBucketDeprecated, + ) + if logBucket == nil { + return nil, ErrNoPastDeltas + } + } + + return logBucket, nil +} + +// DeleteLogBucket deletes the both the new and old revocation log buckets. +func DeleteLogBucket(chanBucket kvdb.RwBucket) error { + // Check if the bucket exists and delete it. + logBucket := chanBucket.NestedReadWriteBucket( + revocationLogBucket, + ) + if logBucket != nil { + err := chanBucket.DeleteNestedBucket(revocationLogBucket) + if err != nil { + return err + } + } + + // We also check whether the old revocation log bucket exists + // and delete it if so. + oldLogBucket := chanBucket.NestedReadWriteBucket( + revocationLogBucketDeprecated, + ) + if oldLogBucket != nil { + err := chanBucket.DeleteNestedBucket( + revocationLogBucketDeprecated, + ) + if err != nil { + return err + } + } + + return nil +} + +// RevocationLogTailCommitHeight returns the commit height at the end of the +// revocation log. +func RevocationLogTailCommitHeight(backend kvdb.Backend, + channel *OpenChannel) (uint64, error) { + + var height uint64 + + // If we haven't created any state updates yet, then we'll exit early as + // there's nothing to be found on disk in the revocation bucket. + if channel.RemoteCommitment.CommitHeight == 0 { + return height, nil + } + + if err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + logBucket, err := FetchLogBucket(chanBucket) + if err != nil { + return err + } + + // Once we have the bucket that stores the revocation log from + // this channel, we'll jump to the _last_ key in bucket. Since + // the key is the commit height, we'll decode the bytes and + // return it. + cursor := logBucket.ReadCursor() + rawHeight, _ := cursor.Last() + height = byteOrder.Uint64(rawHeight) + + return nil + }, func() {}); err != nil { + return height, err + } + + return height, nil +} + +// FindPreviousState scans through the append-only log in an attempt to recover +// the previous channel state indicated by the update number. This method is +// intended to be used for obtaining the relevant data needed to claim all +// funds rightfully spendable in the case of an on-chain broadcast of the +// commitment transaction. +func FindPreviousState(backend kvdb.Backend, channel *OpenChannel, + updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { + + commit := &ChannelCommitment{} + rl := &RevocationLog{} + + err := kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + // Find the revocation log from both the new and the old + // bucket. + r, c, err := FetchRevocationLogCompatible( + chanBucket, updateNum, + ) + if err != nil { + return err + } + + rl = r + commit = c + + return nil + }, func() {}) + if err != nil { + return nil, nil, err + } + + // Either the `rl` or the `commit` is nil here. We return them as-is + // and leave it to the caller to decide its following action. + return rl, commit, nil +} + // Record returns a tlv record for the SparsePayHash. func (s *SparsePayHash) Record() tlv.Record { // We use a zero for the type here, as this'll be used along with the From 607f6bb7abab981357da16c6d1a242875d27b2d2 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 09:14:14 -0300 Subject: [PATCH 44/61] chanstate: move commitment update store Move the local commitment update path into the chanstate KV store implementation. Keep channeldb as the public wrapper and pass the final-HTLC storage option through to preserve existing behavior. --- channeldb/channel.go | 182 ++----------------------------------- chanstate/kv_commitment.go | 139 ++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 173 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 25324c45ce..38e6f7faa7 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -33,14 +33,11 @@ const ( ) var ( - closedChannelBucket = cstate.ClosedChannelBucketKey() - openChannelBucket = cstate.OpenChannelBucketKey() - outpointBucket = cstate.OutpointBucketKey() - chanIDBucket = cstate.ChanIDBucketKey() - historicalChannelBucket = cstate.HistoricalChannelBucketKey() - unsignedAckedUpdatesKey = cstate.UnsignedAckedUpdatesKey() - remoteUnsignedLocalUpdatesKey = cstate.RemoteUnsignedLocalUpdatesKey() - lastWasRevokeKey = cstate.LastWasRevokeKey() + closedChannelBucket = cstate.ClosedChannelBucketKey() + openChannelBucket = cstate.OpenChannelBucketKey() + outpointBucket = cstate.OutpointBucketKey() + chanIDBucket = cstate.ChanIDBucketKey() + historicalChannelBucket = cstate.HistoricalChannelBucketKey() ) var ( @@ -304,17 +301,6 @@ func fetchChanBucket(tx kvdb.RTx, nodeKey *btcec.PublicKey, return cstate.FetchChanBucket(tx, nodeKey, outPoint, chainHash) } -// fetchChanBucketRw is a helper function that returns the bucket where a -// channel's data resides in given: the public key for the node, the outpoint, -// and the chainhash that the channel resides on. This differs from -// fetchChanBucket in that it returns a writeable bucket. -func fetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey, - outPoint *wire.OutPoint, chainHash chainhash.Hash) (kvdb.RwBucket, - error) { - - return cstate.FetchChanBucketRw(tx, nodeKey, outPoint, chainHash) -} - func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, chanID lnwire.ShortChannelID) (kvdb.RwBucket, error) { @@ -414,17 +400,6 @@ func (c *ChannelStateDB) FetchChannelShutdownInfo( return cstate.FetchShutdownInfo(c.backend, channel) } -// isChannelBorked returns true if the channel has been marked as borked in the -// database. This requires an existing database transaction to already be -// active. -// -// NOTE: The primary mutex should already be held before this method is called. -func isChannelBorked(channel *OpenChannel, chanBucket kvdb.RBucket) ( - bool, error) { - - return cstate.IsChannelBorked(channel, chanBucket) -} - // MarkChannelCommitmentBroadcasted marks the channel as having a commitment // transaction broadcast. func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( @@ -552,145 +527,10 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, newCommitment *ChannelCommitment, unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) { - var finalHtlcs = make(map[uint64]bool) - - err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - // If the channel is marked as borked, then for safety reasons, - // we shouldn't attempt any further updates. - isBorked, err := isChannelBorked(channel, chanBucket) - if err != nil { - return err - } - if isBorked { - return ErrChanBorked - } - - if err = putChanInfo(chanBucket, channel); err != nil { - return fmt.Errorf("unable to store chan info: %w", err) - } - - // With the proper bucket fetched, we'll now write the latest - // commitment state to disk for the target party. - err = putChanCommitment( - chanBucket, newCommitment, true, - ) - if err != nil { - return fmt.Errorf("unable to store chan "+ - "revocations: %v", err) - } - - // Persist unsigned but acked remote updates that need to be - // restored after a restart. - var b bytes.Buffer - err = cstate.SerializeLogUpdates(&b, unsignedAckedUpdates) - if err != nil { - return err - } - - err = chanBucket.Put(unsignedAckedUpdatesKey, b.Bytes()) - if err != nil { - return fmt.Errorf("unable to store dangline remote "+ - "updates: %v", err) - } - - //nolint:ll - // Since we have just sent the counterparty a revocation, store true - // under lastWasRevokeKey. - var b2 bytes.Buffer - if err := WriteElements(&b2, true); err != nil { - return err - } - - err = chanBucket.Put(lastWasRevokeKey, b2.Bytes()) - if err != nil { - return err - } - - //nolint:ll - // Persist the remote unsigned local updates that are not included - // in our new commitment. - updateBytes := chanBucket.Get(remoteUnsignedLocalUpdatesKey) - if updateBytes == nil { - return nil - } - - r := bytes.NewReader(updateBytes) - updates, err := cstate.DeserializeLogUpdates(r) - if err != nil { - return err - } - - // Get the bucket where settled htlcs are recorded if the user - // opted in to storing this information. - var finalHtlcsBucket kvdb.RwBucket - if c.parent.storeFinalHtlcResolutions { - bucket, err := fetchFinalHtlcsBucketRw( - tx, channel.ShortChannelID, - ) - if err != nil { - return err - } - - finalHtlcsBucket = bucket - } - - var unsignedUpdates []LogUpdate - for _, upd := range updates { - // Gather updates that are not on our local commitment. - if upd.LogIndex >= newCommitment.LocalLogIndex { - unsignedUpdates = append(unsignedUpdates, upd) - - continue - } - - // The update was locked in. If the update was a - // resolution, then store it in the database. - err := processFinalHtlc( - finalHtlcsBucket, upd, finalHtlcs, - ) - if err != nil { - return err - } - } - - var b3 bytes.Buffer - err = cstate.SerializeLogUpdates(&b3, unsignedUpdates) - if err != nil { - return fmt.Errorf("unable to serialize log updates: %w", - err) - } - - err = chanBucket.Put(remoteUnsignedLocalUpdatesKey, b3.Bytes()) - if err != nil { - return fmt.Errorf("unable to restore chanbucket: %w", - err) - } - - return nil - }, func() { - finalHtlcs = make(map[uint64]bool) - }) - if err != nil { - return nil, err - } - - return finalHtlcs, nil -} - -// processFinalHtlc stores a final htlc outcome in the database if signaled via -// the supplied log update. An in-memory htlcs map is updated too. -func processFinalHtlc(finalHtlcsBucket kvdb.RwBucket, upd LogUpdate, - finalHtlcs map[uint64]bool) error { - - return cstate.ProcessFinalHtlc(finalHtlcsBucket, upd, finalHtlcs) + return cstate.UpdateChannelCommitment( + c.backend, channel, newCommitment, unsignedAckedUpdates, + c.parent.storeFinalHtlcResolutions, + ) } // SerializeHtlcs writes out the passed set of HTLC's into the passed writer @@ -1152,10 +992,6 @@ func deserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) { return cstate.DeserializeCloseChannelSummary(r) } -func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - return cstate.PutChanInfo(chanBucket, channel) -} - func serializeChanCommit(w io.Writer, c *ChannelCommitment) error { return cstate.SerializeChanCommit(w, c) } diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go index 556791331f..68f29c6be1 100644 --- a/chanstate/kv_commitment.go +++ b/chanstate/kv_commitment.go @@ -572,6 +572,145 @@ func InsertNextRevocation(backend kvdb.Backend, channel *OpenChannel, return nil } +// UpdateChannelCommitment updates the local commitment state. +func UpdateChannelCommitment(backend kvdb.Backend, channel *OpenChannel, + newCommitment *ChannelCommitment, + unsignedAckedUpdates []LogUpdate, storeFinalHtlcResolutions bool) ( + map[uint64]bool, error) { + + var finalHtlcs = make(map[uint64]bool) + + err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := FetchChanBucketRw( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + // If the channel is marked as borked, then for safety reasons, + // we shouldn't attempt any further updates. + isBorked, err := IsChannelBorked(channel, chanBucket) + if err != nil { + return err + } + if isBorked { + return ErrChanBorked + } + + if err = PutChanInfo(chanBucket, channel); err != nil { + return fmt.Errorf("unable to store chan info: %w", err) + } + + // With the proper bucket fetched, we'll now write the latest + // commitment state to disk for the target party. + err = PutChanCommitment( + chanBucket, newCommitment, true, + ) + if err != nil { + return fmt.Errorf("unable to store chan "+ + "revocations: %v", err) + } + + // Persist unsigned but acked remote updates that need to be + // restored after a restart. + var b bytes.Buffer + err = SerializeLogUpdates(&b, unsignedAckedUpdates) + if err != nil { + return err + } + + err = chanBucket.Put(unsignedAckedUpdatesKey, b.Bytes()) + if err != nil { + return fmt.Errorf("unable to store dangline remote "+ + "updates: %v", err) + } + + //nolint:ll + // Since we have just sent the counterparty a revocation, store true + // under lastWasRevokeKey. + var b2 bytes.Buffer + if err := WriteElements(&b2, true); err != nil { + return err + } + + err = chanBucket.Put(lastWasRevokeKey, b2.Bytes()) + if err != nil { + return err + } + + //nolint:ll + // Persist the remote unsigned local updates that are not included + // in our new commitment. + updateBytes := chanBucket.Get(remoteUnsignedLocalUpdatesKey) + if updateBytes == nil { + return nil + } + + r := bytes.NewReader(updateBytes) + updates, err := DeserializeLogUpdates(r) + if err != nil { + return err + } + + // Get the bucket where settled htlcs are recorded if the user + // opted in to storing this information. + var finalHtlcsBucket kvdb.RwBucket + if storeFinalHtlcResolutions { + bucket, err := FetchFinalHtlcsBucketRw( + tx, channel.ShortChannelID, + ) + if err != nil { + return err + } + + finalHtlcsBucket = bucket + } + + var unsignedUpdates []LogUpdate + for _, upd := range updates { + // Gather updates that are not on our local commitment. + if upd.LogIndex >= newCommitment.LocalLogIndex { + unsignedUpdates = append(unsignedUpdates, upd) + + continue + } + + // The update was locked in. If the update was a + // resolution, then store it in the database. + err := ProcessFinalHtlc( + finalHtlcsBucket, upd, finalHtlcs, + ) + if err != nil { + return err + } + } + + var b3 bytes.Buffer + err = SerializeLogUpdates(&b3, unsignedUpdates) + if err != nil { + return fmt.Errorf("unable to serialize log updates: %w", + err) + } + + err = chanBucket.Put(remoteUnsignedLocalUpdatesKey, b3.Bytes()) + if err != nil { + return fmt.Errorf("unable to restore chanbucket: %w", + err) + } + + return nil + }, func() { + finalHtlcs = make(map[uint64]bool) + }) + if err != nil { + return nil, err + } + + return finalHtlcs, nil +} + // AppendRemoteCommitChain appends a new CommitDiff to the remote party's // commitment chain. func AppendRemoteCommitChain(backend kvdb.Backend, channel *OpenChannel, From 904ab7705260951736420a5712a7a2671d7ad689 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 09:32:11 -0300 Subject: [PATCH 45/61] chanstate: move close channel kv store Move the close/archive channel paths into the chanstate KV store implementation. Keep channeldb as the public wrapper and preserve the existing sync/tombstone strategy selection at the boundary. --- channeldb/channel.go | 253 +----------------------------- channeldb/legacy_serialization.go | 4 +- chanstate/kv_close_summary.go | 198 +++++++++++++++++++++++ 3 files changed, 208 insertions(+), 247 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 38e6f7faa7..6542a1c226 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1,7 +1,6 @@ package channeldb import ( - "bytes" "fmt" "io" "net" @@ -11,7 +10,6 @@ import ( "github.com/btcsuite/btcd/wire/v2" cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" - graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -461,12 +459,6 @@ func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, return cstate.ClearChannelStatus(c.backend, channel, status) } -// putOpenChannel serializes, and stores the current state of the channel in its -// entirety. -func putOpenChannel(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - return cstate.PutOpenChannel(chanBucket, channel) -} - // fetchOpenChannel retrieves, and deserializes (including decrypting // sensitive) the complete channel currently active with the passed nodeID. func fetchOpenChannel(chanBucket kvdb.RBucket, @@ -572,10 +564,6 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { return cstate.DeserializeHtlcs(r) } -func newChannelPackager(channel *OpenChannel) *ChannelPackager { - return NewChannelPackager(channel.ShortChannelID) -} - // AppendRemoteCommitChain appends a new CommitDiff to the remote party's // commitment chain. func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, @@ -652,6 +640,8 @@ func (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, // AckAddHtlcs updates the AckAddFilter containing any of the provided AddRefs // indicating that a response to this Add has been committed to the remote party. // Doing so will prevent these Add HTLCs from being reforwarded internally. +// +//nolint:ll func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, addRefs ...AddRef) error { @@ -681,6 +671,8 @@ func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, // later packages won't be removed. // // NOTE: This method should only be called on packages marked FwdStateCompleted. +// +//nolint:ll func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, heights ...uint64) error { @@ -760,197 +752,10 @@ type ChannelCloseSummary = cstate.ChannelCloseSummary func (c *ChannelStateDB) CloseChannel(channel *OpenChannel, summary *ChannelCloseSummary, statuses ...ChannelStatus) error { - if c.tombstoneClosedChannels { - return c.closeChannelTombstone(channel, summary, statuses...) - } - - return c.closeChannelSync(channel, summary, statuses...) -} - -// locateOpenChannel performs the open-channel-bucket descent for a -// CloseChannel transaction: it returns the chain bucket, the channel bucket, -// and the serialized chanKey for the supplied OpenChannel. A chanKey already -// flipped to outpointClosed surfaces ErrChannelNotFound so a redundant -// CloseChannel does not re-archive or re-flip the index. -func locateOpenChannel(tx kvdb.RwTx, channel *OpenChannel) (kvdb.RwBucket, - kvdb.RwBucket, []byte, error) { - - openChanBucket := tx.ReadWriteBucket(openChannelBucket) - if openChanBucket == nil { - return nil, nil, nil, ErrNoChanDBExists - } - - nodePub := channel.IdentityPub.SerializeCompressed() - nodeChanBucket := openChanBucket.NestedReadWriteBucket(nodePub) - if nodeChanBucket == nil { - return nil, nil, nil, ErrNoActiveChannels - } - - chainBucket := nodeChanBucket.NestedReadWriteBucket( - channel.ChainHash[:], - ) - if chainBucket == nil { - return nil, nil, nil, ErrNoActiveChannels - } - - var chanPointBuf bytes.Buffer - if err := graphdb.WriteOutpoint( - &chanPointBuf, &channel.FundingOutpoint, - ); err != nil { - return nil, nil, nil, err - } - chanKey := chanPointBuf.Bytes() - - chanBucket := chainBucket.NestedReadWriteBucket(chanKey) - if chanBucket == nil { - return nil, nil, nil, ErrNoActiveChannels - } - - // A channel whose outpoint is already flipped to outpointClosed must - // not be re-closed: on tombstone backends the chanBucket survives a - // previous close, but the index flip is the authoritative record that - // the channel is gone from the open-channel view. - closed, err := isOutpointClosed(tx.ReadBucket(outpointBucket), chanKey) - if err != nil { - return nil, nil, nil, err - } - if closed { - return nil, nil, nil, ErrChannelNotFound - } - - return chainBucket, chanBucket, chanKey, nil -} - -// updateClosedOutpointIndex flips the outpoint index entry for chanKey from -// open to closed. The index entry must already exist; it was placed there -// when the channel was opened. -func updateClosedOutpointIndex(tx kvdb.RwTx, chanKey []byte) error { - return cstate.UpdateClosedOutpointIndex(tx, chanKey) -} - -// archiveClosedChannel writes the immutable close-time records of the -// channel: a copy of the open-channel state under historicalChannelBucket -// (with the supplied close statuses OR'd into chanStatus) and the close -// summary under closeSummaryBucket. -func archiveClosedChannel(tx kvdb.RwTx, chanKey []byte, - chanState *OpenChannel, summary *ChannelCloseSummary, - statuses ...ChannelStatus) error { - - historicalBucket, err := tx.CreateTopLevelBucket( - historicalChannelBucket, + return cstate.CloseChannel( + c.backend, channel, summary, c.tombstoneClosedChannels, + statuses..., ) - if err != nil { - return err - } - historicalChanBucket, err := historicalBucket.CreateBucketIfNotExists( - chanKey, - ) - if err != nil { - return err - } - - for _, s := range statuses { - chanState.SetChannelStatusForStore( - chanState.ChannelStatusForStore() | s, - ) - } - - if err := putOpenChannel(historicalChanBucket, chanState); err != nil { - return err - } - - return putChannelCloseSummary(tx, chanKey, summary, chanState) -} - -// closeChannelSync performs the historical synchronous close path: in a -// single write transaction it wipes the forwarding-package state, deletes -// the channel bucket and its nested revocation log entries, updates the -// outpoint index, and archives the close summary. It is used by backends -// where nested-bucket deletion is cheap (bbolt, etcd). -func (c *ChannelStateDB) closeChannelSync(channel *OpenChannel, - summary *ChannelCloseSummary, statuses ...ChannelStatus) error { - - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - chainBucket, chanBucket, chanKey, err := locateOpenChannel( - tx, channel, - ) - if err != nil { - return err - } - - chanState, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - packager := NewChannelPackager(chanState.ShortChannelID) - if err = packager.Wipe(tx); err != nil { - return err - } - - if err := deleteOpenChannel(chanBucket); err != nil { - return err - } - - if channel.ChanType.IsFrozen() || - channel.ChanType.HasLeaseExpiration() { - - if err := deleteThawHeight(chanBucket); err != nil { - return err - } - } - - if err := deleteLogBucket(chanBucket); err != nil { - return err - } - - if err := chainBucket.DeleteNestedBucket(chanKey); err != nil { - return err - } - - if err := updateClosedOutpointIndex(tx, chanKey); err != nil { - return err - } - - return archiveClosedChannel( - tx, chanKey, chanState, summary, statuses..., - ) - }, func() {}) -} - -// closeChannelTombstone performs the tombstone close path used by -// KV-over-SQL backends. The channel's per-channel state is left intact — -// touching it would trigger the cascading nested-bucket delete this path -// exists to avoid — and the outpointBucket flip from outpointOpen to -// outpointClosed serves as the authoritative closed-channel marker. The -// disk space is reclaimed wholesale by the upcoming native-SQL -// channel-state migration. -func (c *ChannelStateDB) closeChannelTombstone(channel *OpenChannel, - summary *ChannelCloseSummary, statuses ...ChannelStatus) error { - - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - _, chanBucket, chanKey, err := locateOpenChannel(tx, channel) - if err != nil { - return err - } - - chanState, err := fetchOpenChannel( - chanBucket, &channel.FundingOutpoint, - ) - if err != nil { - return err - } - - if err := updateClosedOutpointIndex(tx, chanKey); err != nil { - return err - } - - return archiveClosedChannel( - tx, chanKey, chanState, summary, statuses..., - ) - }, func() {}) } // ChannelSnapshot is a frozen snapshot of the current channel state. @@ -976,14 +781,6 @@ func (c *ChannelStateDB) RemoteRevocationStore(channel *OpenChannel) ( return cstate.RemoteRevocationStore(c.backend, channel) } -func putChannelCloseSummary(tx kvdb.RwTx, chanID []byte, - summary *ChannelCloseSummary, lastChanState *OpenChannel) error { - - return cstate.PutChannelCloseSummary( - tx, chanID, summary, lastChanState, - ) -} - func serializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error { return cstate.SerializeChannelCloseSummary(w, cs) } @@ -996,38 +793,10 @@ func serializeChanCommit(w io.Writer, c *ChannelCommitment) error { return cstate.SerializeChanCommit(w, c) } -func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, - local bool) error { - - return cstate.PutChanCommitment(chanBucket, c, local) -} - -func putChanCommitments(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - return cstate.PutChanCommitments(chanBucket, channel) -} - -func putChanRevocationState(chanBucket kvdb.RwBucket, channel *OpenChannel) error { - return cstate.PutChanRevocationState(chanBucket, channel) -} - -func readChanConfig(b io.Reader, c *ChannelConfig) error { - return cstate.ReadChanConfig(b, c) -} - func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { return cstate.FetchChanInfo(chanBucket, channel) } -func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) { - return cstate.DeserializeChanCommit(r) -} - -func fetchChanCommitment(chanBucket kvdb.RBucket, - local bool) (ChannelCommitment, error) { - - return cstate.FetchChanCommitment(chanBucket, local) -} - func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { return cstate.FetchChanCommitments(chanBucket, channel) } @@ -1036,10 +805,6 @@ func fetchChanRevocationState(chanBucket kvdb.RBucket, channel *OpenChannel) err return cstate.FetchChanRevocationState(chanBucket, channel) } -func deleteOpenChannel(chanBucket kvdb.RwBucket) error { - return cstate.DeleteOpenChannel(chanBucket) -} - // makeLogKey converts a uint64 into an 8 byte array. func makeLogKey(updateNum uint64) [8]byte { var key [8]byte @@ -1055,10 +820,6 @@ func storeThawHeight(chanBucket kvdb.RwBucket, height uint32) error { return cstate.StoreThawHeight(chanBucket, height) } -func deleteThawHeight(chanBucket kvdb.RwBucket) error { - return cstate.DeleteThawHeight(chanBucket) -} - // EKeyLocator is an encoder for keychain.KeyLocator. func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { return cstate.EKeyLocator(w, val, buf) diff --git a/channeldb/legacy_serialization.go b/channeldb/legacy_serialization.go index c2e636c5ea..9bfa1180a7 100644 --- a/channeldb/legacy_serialization.go +++ b/channeldb/legacy_serialization.go @@ -2,6 +2,8 @@ package channeldb import ( "io" + + cstate "github.com/lightningnetwork/lnd/chanstate" ) // deserializeCloseChannelSummaryV6 reads the v6 database format for @@ -34,7 +36,7 @@ func deserializeCloseChannelSummaryV6(r io.Reader) (*ChannelCloseSummary, error) return nil, err } - if err := readChanConfig(r, &c.LocalChanConfig); err != nil { + if err := cstate.ReadChanConfig(r, &c.LocalChanConfig); err != nil { return nil, err } diff --git a/chanstate/kv_close_summary.go b/chanstate/kv_close_summary.go index a11abe2cb4..4a85942802 100644 --- a/chanstate/kv_close_summary.go +++ b/chanstate/kv_close_summary.go @@ -5,6 +5,7 @@ import ( "errors" "io" + graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" ) @@ -31,6 +32,203 @@ func PutChannelCloseSummary(tx kvdb.RwTx, chanID []byte, return closedChanBucket.Put(chanID, b.Bytes()) } +// CloseChannel closes the supplied channel via the selected close strategy. On +// synchronous backends the channel's nested state — the revocation log, the +// per-channel forwarding-package bucket, and the chanBucket itself — is deleted +// inline. On tombstone-enabled backends none of the bulk state is touched; the +// outpointBucket flip to outpointClosed signals that the channel is logically +// closed. +func CloseChannel(backend kvdb.Backend, channel *OpenChannel, + summary *ChannelCloseSummary, tombstoneClosedChannels bool, + statuses ...ChannelStatus) error { + + if tombstoneClosedChannels { + return CloseChannelTombstone( + backend, channel, summary, statuses..., + ) + } + + return CloseChannelSync(backend, channel, summary, statuses...) +} + +// LocateOpenChannel performs the open-channel-bucket descent for a CloseChannel +// transaction: it returns the chain bucket, the channel bucket, and the +// serialized chanKey for the supplied OpenChannel. A chanKey already flipped to +// outpointClosed surfaces ErrChannelNotFound so a redundant CloseChannel does +// not re-archive or re-flip the index. +func LocateOpenChannel(tx kvdb.RwTx, channel *OpenChannel) (kvdb.RwBucket, + kvdb.RwBucket, []byte, error) { + + openChanBucket := tx.ReadWriteBucket(openChannelBucket) + if openChanBucket == nil { + return nil, nil, nil, ErrNoChanDBExists + } + + nodePub := channel.IdentityPub.SerializeCompressed() + nodeChanBucket := openChanBucket.NestedReadWriteBucket(nodePub) + if nodeChanBucket == nil { + return nil, nil, nil, ErrNoActiveChannels + } + + chainBucket := nodeChanBucket.NestedReadWriteBucket( + channel.ChainHash[:], + ) + if chainBucket == nil { + return nil, nil, nil, ErrNoActiveChannels + } + + var chanPointBuf bytes.Buffer + if err := graphdb.WriteOutpoint( + &chanPointBuf, &channel.FundingOutpoint, + ); err != nil { + return nil, nil, nil, err + } + chanKey := chanPointBuf.Bytes() + + chanBucket := chainBucket.NestedReadWriteBucket(chanKey) + if chanBucket == nil { + return nil, nil, nil, ErrNoActiveChannels + } + + // A channel whose outpoint is already flipped to outpointClosed must + // not be re-closed: on tombstone backends the chanBucket survives a + // previous close, but the index flip is the authoritative record that + // the channel is gone from the open-channel view. + closed, err := IsOutpointClosed(tx.ReadBucket(outpointBucket), chanKey) + if err != nil { + return nil, nil, nil, err + } + if closed { + return nil, nil, nil, ErrChannelNotFound + } + + return chainBucket, chanBucket, chanKey, nil +} + +// ArchiveClosedChannel writes the immutable close-time records of the channel: +// a copy of the open-channel state under historicalChannelBucket (with the +// supplied close statuses OR'd into chanStatus) and the close summary under +// closeSummaryBucket. +func ArchiveClosedChannel(tx kvdb.RwTx, chanKey []byte, + chanState *OpenChannel, summary *ChannelCloseSummary, + statuses ...ChannelStatus) error { + + historicalBucket, err := tx.CreateTopLevelBucket( + historicalChannelBucket, + ) + if err != nil { + return err + } + historicalChanBucket, err := historicalBucket.CreateBucketIfNotExists( + chanKey, + ) + if err != nil { + return err + } + + for _, s := range statuses { + chanState.SetChannelStatusForStore( + chanState.ChannelStatusForStore() | s, + ) + } + + if err := PutOpenChannel(historicalChanBucket, chanState); err != nil { + return err + } + + return PutChannelCloseSummary(tx, chanKey, summary, chanState) +} + +// CloseChannelSync performs the historical synchronous close path: in a single +// write transaction it wipes the forwarding-package state, deletes the channel +// bucket and its nested revocation log entries, updates the outpoint index, and +// archives the close summary. It is used by backends where nested-bucket +// deletion is cheap (bbolt, etcd). +func CloseChannelSync(backend kvdb.Backend, channel *OpenChannel, + summary *ChannelCloseSummary, statuses ...ChannelStatus) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + chainBucket, chanBucket, chanKey, err := LocateOpenChannel( + tx, channel, + ) + if err != nil { + return err + } + + chanState, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + packager := NewChannelPackager(chanState.ShortChannelID) + if err = packager.Wipe(tx); err != nil { + return err + } + + if err := DeleteOpenChannel(chanBucket); err != nil { + return err + } + + if channel.ChanType.IsFrozen() || + channel.ChanType.HasLeaseExpiration() { + + if err := DeleteThawHeight(chanBucket); err != nil { + return err + } + } + + if err := DeleteLogBucket(chanBucket); err != nil { + return err + } + + if err := chainBucket.DeleteNestedBucket(chanKey); err != nil { + return err + } + + if err := UpdateClosedOutpointIndex(tx, chanKey); err != nil { + return err + } + + return ArchiveClosedChannel( + tx, chanKey, chanState, summary, statuses..., + ) + }, func() {}) +} + +// CloseChannelTombstone performs the tombstone close path used by KV-over-SQL +// backends. The channel's per-channel state is left intact — touching it would +// trigger the cascading nested-bucket delete this path exists to avoid — and +// the outpointBucket flip from outpointOpen to outpointClosed serves as the +// authoritative closed-channel marker. The disk space is reclaimed wholesale by +// the upcoming native-SQL channel-state migration. +func CloseChannelTombstone(backend kvdb.Backend, channel *OpenChannel, + summary *ChannelCloseSummary, statuses ...ChannelStatus) error { + + return kvdb.Update(backend, func(tx kvdb.RwTx) error { + _, chanBucket, chanKey, err := LocateOpenChannel(tx, channel) + if err != nil { + return err + } + + chanState, err := FetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) + if err != nil { + return err + } + + if err := UpdateClosedOutpointIndex(tx, chanKey); err != nil { + return err + } + + return ArchiveClosedChannel( + tx, chanKey, chanState, summary, statuses..., + ) + }, func() {}) +} + // SerializeChannelCloseSummary serializes a channel close summary. func SerializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error { From 3d0f510f3b049b044929090925522326c5fbdef6 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 09:36:49 -0300 Subject: [PATCH 46/61] chanstate: move refresh channel store Move the refresh path into the chanstate open-channel KV store implementation. Keep channeldb as a public wrapper and remove private helpers that were only used by the old refresh implementation. --- channeldb/channel.go | 57 +----------------------------------- chanstate/kv_open_channel.go | 37 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 56 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 6542a1c226..b812deb57d 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1,12 +1,10 @@ package channeldb import ( - "fmt" "io" "net" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/wire/v2" cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" @@ -255,48 +253,7 @@ const ( // RefreshChannel updates the in-memory channel state using the latest state // observed on disk. func (c *ChannelStateDB) RefreshChannel(channel *OpenChannel) error { - return kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchChanBucket( - tx, channel.IdentityPub, &channel.FundingOutpoint, - channel.ChainHash, - ) - if err != nil { - return err - } - - // We'll re-populating the in-memory channel with the info - // fetched from disk. - if err := fetchChanInfo(chanBucket, channel); err != nil { - return fmt.Errorf("unable to fetch chan info: %w", err) - } - - // Also populate the channel's commitment states for both sides - // of the channel. - err = fetchChanCommitments(chanBucket, channel) - if err != nil { - return fmt.Errorf("unable to fetch chan commitments: "+ - "%v", err) - } - - // Also retrieve the current revocation state. - err = fetchChanRevocationState(chanBucket, channel) - if err != nil { - return fmt.Errorf("unable to fetch chan revocations: "+ - "%v", err) - } - - return nil - }, func() {}) -} - -// fetchChanBucket is a helper function that returns the bucket where a -// channel's data resides in given: the public key for the node, the outpoint, -// and the chainhash that the channel resides on. -func fetchChanBucket(tx kvdb.RTx, nodeKey *btcec.PublicKey, - outPoint *wire.OutPoint, chainHash chainhash.Hash) ( - kvdb.RBucket, error) { - - return cstate.FetchChanBucket(tx, nodeKey, outPoint, chainHash) + return cstate.RefreshChannel(c.backend, channel) } func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, @@ -793,18 +750,6 @@ func serializeChanCommit(w io.Writer, c *ChannelCommitment) error { return cstate.SerializeChanCommit(w, c) } -func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { - return cstate.FetchChanInfo(chanBucket, channel) -} - -func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { - return cstate.FetchChanCommitments(chanBucket, channel) -} - -func fetchChanRevocationState(chanBucket kvdb.RBucket, channel *OpenChannel) error { - return cstate.FetchChanRevocationState(chanBucket, channel) -} - // makeLogKey converts a uint64 into an 8 byte array. func makeLogKey(updateNum uint64) [8]byte { var key [8]byte diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index e4e9179470..bd7edcf547 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -681,6 +681,43 @@ func FetchOpenChannel(chanBucket kvdb.RBucket, return channel, nil } +// RefreshChannel updates the in-memory channel state using the latest state +// observed on disk. +func RefreshChannel(backend kvdb.Backend, channel *OpenChannel) error { + return kvdb.View(backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchChanBucket( + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, + ) + if err != nil { + return err + } + + // We'll re-populating the in-memory channel with the info + // fetched from disk. + if err := FetchChanInfo(chanBucket, channel); err != nil { + return fmt.Errorf("unable to fetch chan info: %w", err) + } + + // Also populate the channel's commitment states for both sides + // of the channel. + err = FetchChanCommitments(chanBucket, channel) + if err != nil { + return fmt.Errorf("unable to fetch chan commitments: "+ + "%v", err) + } + + // Also retrieve the current revocation state. + err = FetchChanRevocationState(chanBucket, channel) + if err != nil { + return fmt.Errorf("unable to fetch chan revocations: "+ + "%v", err) + } + + return nil + }, func() {}) +} + // MarkChannelConfirmationHeight updates the channel's confirmation height once // the channel opening transaction receives one confirmation. func MarkChannelConfirmationHeight(backend kvdb.Backend, From 69ddbf28975975fc3bc8d5f10993cb30a147ddad Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 09:50:07 -0300 Subject: [PATCH 47/61] chanstate: move pending channel sync Move the pending open-channel write into the chanstate KV store implementation while keeping link-node creation in channeldb. Preserve the existing transaction boundary used by pending channels and restores. --- channeldb/channel.go | 18 +++++++----------- channeldb/db.go | 1 + chanstate/kv_open_channel.go | 10 ++++++++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index b812deb57d..e5bf64b5ed 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -262,12 +262,6 @@ func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, return cstate.FetchFinalHtlcsBucketRw(tx, chanID) } -// fullSyncOpenChannel syncs the contents of an OpenChannel while re-using an -// existing database transaction. -func fullSyncOpenChannel(tx kvdb.RwTx, c *OpenChannel) error { - return cstate.FullSyncOpenChannel(tx, c) -} - // MarkChannelConfirmationHeight updates the channel's confirmation height once // the channel opening transaction receives one confirmation. func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, @@ -429,20 +423,22 @@ func fetchOpenChannel(chanBucket kvdb.RBucket, func (c *ChannelStateDB) SyncPendingChannel(channel *OpenChannel, addr net.Addr, pendingHeight uint32) error { - channel.FundingBroadcastHeight = pendingHeight - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - return syncNewChannel(tx, channel, []net.Addr{addr}, c.backend) + return syncNewChannel( + tx, channel, []net.Addr{addr}, c.backend, + pendingHeight, + ) }, func() {}) } // syncNewChannel will write the passed channel to disk, and also create a // LinkNode (if needed) for the channel peer. func syncNewChannel(tx kvdb.RwTx, c *OpenChannel, addrs []net.Addr, - backend kvdb.Backend) error { + backend kvdb.Backend, pendingHeight uint32) error { // First, sync all the persistent channel state to disk. - if err := fullSyncOpenChannel(tx, c); err != nil { + err := cstate.SyncPendingOpenChannel(tx, c, pendingHeight) + if err != nil { return err } diff --git a/channeldb/db.go b/channeldb/db.go index ab8e0b01d2..6a90cd6222 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -1699,6 +1699,7 @@ func (c *ChannelStateDB) RestoreChannelShells(channelShells ...*ChannelShell) er channel.Db = c err := syncNewChannel( tx, channel, channelShell.NodeAddrs, c.backend, + channel.FundingBroadcastHeight, ) if err != nil { return err diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index bd7edcf547..2ce4799e88 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -681,6 +681,16 @@ func FetchOpenChannel(chanBucket kvdb.RBucket, return channel, nil } +// SyncPendingOpenChannel writes a pending channel to the store and records the +// funding broadcast height using an existing database transaction. +func SyncPendingOpenChannel(tx kvdb.RwTx, channel *OpenChannel, + pendingHeight uint32) error { + + channel.FundingBroadcastHeight = pendingHeight + + return FullSyncOpenChannel(tx, channel) +} + // RefreshChannel updates the in-memory channel state using the latest state // observed on disk. func RefreshChannel(backend kvdb.Backend, channel *OpenChannel) error { From cb5b8579225802ff914e71ebf77aec2eac3fe3f2 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 10:12:06 -0300 Subject: [PATCH 48/61] chanstate: fold channel kv helper files Fold the channel key and thaw-height helpers into the store files that own them, instead of keeping separate kv files that do not represent their own stores. The open-channel keys and thaw-height helpers now live with kv_open_channel, closed-channel bucket ownership lives with kv_close_summary, and commitment/update keys live with kv_commitment. --- chanstate/kv_channel_keys.go | 169 --------------------------------- chanstate/kv_close_summary.go | 12 +++ chanstate/kv_commitment.go | 74 +++++++++++++++ chanstate/kv_open_channel.go | 174 +++++++++++++++++++++++++++++----- chanstate/kv_thaw_height.go | 49 ---------- 5 files changed, 235 insertions(+), 243 deletions(-) delete mode 100644 chanstate/kv_channel_keys.go delete mode 100644 chanstate/kv_thaw_height.go diff --git a/chanstate/kv_channel_keys.go b/chanstate/kv_channel_keys.go deleted file mode 100644 index 502f450c4c..0000000000 --- a/chanstate/kv_channel_keys.go +++ /dev/null @@ -1,169 +0,0 @@ -package chanstate - -var ( - // closedChannelBucket stores summarization information concerning - // previously open, but now closed channels. - closedChannelBucket = []byte("closed-chan-bucket") - - // openChannelBucket stores all the currently open channels. This bucket - // has a second, nested bucket which is keyed by a node's ID. Within - // that node ID bucket, all attributes required to track, update, and - // close a channel are stored. - // - // openChan -> nodeID -> chanPoint - // - // TODO(roasbeef): flesh out comment. - openChannelBucket = []byte("open-chan-bucket") - - // outpointBucket stores all of our channel outpoints and a tlv - // stream containing channel data. - // - // outpoint -> tlv stream. - // - outpointBucket = []byte("outpoint-bucket") - - // chanIDBucket stores all of the 32-byte channel ID's we know about. - // These could be derived from outpointBucket, but it is more - // convenient to have these in their own bucket. - // - // chanID -> tlv stream. - // - chanIDBucket = []byte("chan-id-bucket") - - // historicalChannelBucket stores all channels that have seen their - // commitment tx confirm. All information from their previous open state - // is retained. - historicalChannelBucket = []byte("historical-chan-bucket") - - // chanInfoKey can be accessed within the bucket for a channel - // (identified by its chanPoint). This key stores all the static - // information for a channel which is decided at the end of the - // funding flow. - chanInfoKey = []byte("chan-info-key") - - // localUpfrontShutdownKey can be accessed within the bucket for a - // channel (identified by its chanPoint). This key stores an optional - // upfront shutdown script for the local peer. - localUpfrontShutdownKey = []byte("local-upfront-shutdown-key") - - // remoteUpfrontShutdownKey can be accessed within the bucket for a - // channel (identified by its chanPoint). This key stores an optional - // upfront shutdown script for the remote peer. - remoteUpfrontShutdownKey = []byte("remote-upfront-shutdown-key") - - // chanCommitmentKey can be accessed within the sub-bucket for a - // particular channel. This key stores the up to date commitment state - // for a particular channel party. Appending a 0 to the end of this key - // indicates it's the commitment for the local party, and appending a 1 - // to the end of this key indicates it's the commitment for the remote - // party. - chanCommitmentKey = []byte("chan-commitment-key") - - // unsignedAckedUpdatesKey is an entry in the channel bucket that - // contains the remote updates that we have acked, but not yet signed - // for in one of our remote commits. - unsignedAckedUpdatesKey = []byte("unsigned-acked-updates-key") - - // remoteUnsignedLocalUpdatesKey is an entry in the channel bucket that - // contains the local updates that the remote party has acked, but - // has not yet signed for in one of their local commits. - remoteUnsignedLocalUpdatesKey = []byte( - "remote-unsigned-local-updates-key", - ) - - // revocationStateKey stores their current revocation hash, our - // preimage producer and their preimage store. - revocationStateKey = []byte("revocation-state-key") - - // commitDiffKey stores the current pending commitment state we've - // extended to the remote party (if any). Each time we propose a new - // state, we store the information necessary to reconstruct this state - // from the prior commitment. This allows us to resync the remote party - // to their expected state in the case of message loss. - // - // TODO(roasbeef): rename to commit chain? - commitDiffKey = []byte("commit-diff-key") - - // lastWasRevokeKey is a key that stores true when the last update we - // sent was a revocation and false when it was a commitment signature. - // This is nil in the case of new channels with no updates exchanged. - lastWasRevokeKey = []byte("last-was-revoke") -) - -// ClosedChannelBucketKey returns the top-level closed-channel summary bucket -// key. -func ClosedChannelBucketKey() []byte { - return closedChannelBucket -} - -// OpenChannelBucketKey returns the top-level open-channel bucket key. -func OpenChannelBucketKey() []byte { - return openChannelBucket -} - -// OutpointBucketKey returns the top-level outpoint index bucket key. -func OutpointBucketKey() []byte { - return outpointBucket -} - -// ChanIDBucketKey returns the top-level channel ID index bucket key. -func ChanIDBucketKey() []byte { - return chanIDBucket -} - -// HistoricalChannelBucketKey returns the top-level historical channel bucket -// key. -func HistoricalChannelBucketKey() []byte { - return historicalChannelBucket -} - -// ChanInfoKey returns the channel-bucket key for static channel information. -func ChanInfoKey() []byte { - return chanInfoKey -} - -// LocalUpfrontShutdownKey returns the channel-bucket key for the local upfront -// shutdown script. -func LocalUpfrontShutdownKey() []byte { - return localUpfrontShutdownKey -} - -// RemoteUpfrontShutdownKey returns the channel-bucket key for the remote -// upfront shutdown script. -func RemoteUpfrontShutdownKey() []byte { - return remoteUpfrontShutdownKey -} - -// ChanCommitmentKey returns the channel-bucket key prefix for channel -// commitments. -func ChanCommitmentKey() []byte { - return chanCommitmentKey -} - -// UnsignedAckedUpdatesKey returns the channel-bucket key for unsigned acked -// remote updates. -func UnsignedAckedUpdatesKey() []byte { - return unsignedAckedUpdatesKey -} - -// RemoteUnsignedLocalUpdatesKey returns the channel-bucket key for remote -// unsigned local updates. -func RemoteUnsignedLocalUpdatesKey() []byte { - return remoteUnsignedLocalUpdatesKey -} - -// RevocationStateKey returns the channel-bucket key for revocation state. -func RevocationStateKey() []byte { - return revocationStateKey -} - -// CommitDiffKey returns the channel-bucket key for the current pending -// commitment diff. -func CommitDiffKey() []byte { - return commitDiffKey -} - -// LastWasRevokeKey returns the channel-bucket key for the last update type. -func LastWasRevokeKey() []byte { - return lastWasRevokeKey -} diff --git a/chanstate/kv_close_summary.go b/chanstate/kv_close_summary.go index 4a85942802..0db52ad4d5 100644 --- a/chanstate/kv_close_summary.go +++ b/chanstate/kv_close_summary.go @@ -10,6 +10,18 @@ import ( "github.com/lightningnetwork/lnd/lnwire" ) +var ( + // closedChannelBucket stores summarization information concerning + // previously open, but now closed channels. + closedChannelBucket = []byte("closed-chan-bucket") +) + +// ClosedChannelBucketKey returns the top-level closed-channel summary bucket +// key. +func ClosedChannelBucketKey() []byte { + return closedChannelBucket +} + // PutChannelCloseSummary writes the immutable close-time summary of a channel // under the closed channel bucket. func PutChannelCloseSummary(tx kvdb.RwTx, chanID []byte, diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go index 68f29c6be1..c4e638e05c 100644 --- a/chanstate/kv_commitment.go +++ b/chanstate/kv_commitment.go @@ -16,6 +16,80 @@ import ( "github.com/lightningnetwork/lnd/tlv" ) +var ( + // chanCommitmentKey can be accessed within the sub-bucket for a + // particular channel. This key stores the up to date commitment state + // for a particular channel party. Appending a 0 to the end of this key + // indicates it's the commitment for the local party, and appending a 1 + // to the end of this key indicates it's the commitment for the remote + // party. + chanCommitmentKey = []byte("chan-commitment-key") + + // unsignedAckedUpdatesKey is an entry in the channel bucket that + // contains the remote updates that we have acked, but not yet signed + // for in one of our remote commits. + unsignedAckedUpdatesKey = []byte("unsigned-acked-updates-key") + + // remoteUnsignedLocalUpdatesKey is an entry in the channel bucket that + // contains the local updates that the remote party has acked, but + // has not yet signed for in one of their local commits. + remoteUnsignedLocalUpdatesKey = []byte( + "remote-unsigned-local-updates-key", + ) + + // revocationStateKey stores their current revocation hash, our + // preimage producer and their preimage store. + revocationStateKey = []byte("revocation-state-key") + + // commitDiffKey stores the current pending commitment state we've + // extended to the remote party (if any). Each time we propose a new + // state, we store the information necessary to reconstruct this state + // from the prior commitment. This allows us to resync the remote party + // to their expected state in the case of message loss. + // + // TODO(roasbeef): rename to commit chain? + commitDiffKey = []byte("commit-diff-key") + + // lastWasRevokeKey is a key that stores true when the last update we + // sent was a revocation and false when it was a commitment signature. + // This is nil in the case of new channels with no updates exchanged. + lastWasRevokeKey = []byte("last-was-revoke") +) + +// ChanCommitmentKey returns the channel-bucket key prefix for channel +// commitments. +func ChanCommitmentKey() []byte { + return chanCommitmentKey +} + +// UnsignedAckedUpdatesKey returns the channel-bucket key for unsigned acked +// remote updates. +func UnsignedAckedUpdatesKey() []byte { + return unsignedAckedUpdatesKey +} + +// RemoteUnsignedLocalUpdatesKey returns the channel-bucket key for remote +// unsigned local updates. +func RemoteUnsignedLocalUpdatesKey() []byte { + return remoteUnsignedLocalUpdatesKey +} + +// RevocationStateKey returns the channel-bucket key for revocation state. +func RevocationStateKey() []byte { + return revocationStateKey +} + +// CommitDiffKey returns the channel-bucket key for the current pending +// commitment diff. +func CommitDiffKey() []byte { + return commitDiffKey +} + +// LastWasRevokeKey returns the channel-bucket key for the last update type. +func LastWasRevokeKey() []byte { + return lastWasRevokeKey +} + // serializeHtlcExtraData encodes a TLV stream of extra data to be stored with a // HTLC. It uses the update_add_htlc TLV types, because this is where extra // data is passed with a HTLC. At present blinding points are the only extra diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index 2ce4799e88..44ca0498fb 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -2,6 +2,7 @@ package chanstate import ( "bytes" + "encoding/binary" "errors" "fmt" "io" @@ -18,44 +19,109 @@ import ( ) var ( + // openChannelBucket stores all the currently open channels. This bucket + // has a second, nested bucket which is keyed by a node's ID. Within + // that node ID bucket, all attributes required to track, update, and + // close a channel are stored. + // + // openChan -> nodeID -> chanPoint + // + // TODO(roasbeef): flesh out comment. + openChannelBucket = []byte("open-chan-bucket") + + // outpointBucket stores all of our channel outpoints and a tlv + // stream containing channel data. + // + // outpoint -> tlv stream. + // + outpointBucket = []byte("outpoint-bucket") + + // chanIDBucket stores all of the 32-byte channel ID's we know about. + // These could be derived from outpointBucket, but it is more + // convenient to have these in their own bucket. + // + // chanID -> tlv stream. + // + chanIDBucket = []byte("chan-id-bucket") + + // historicalChannelBucket stores all channels that have seen their + // commitment tx confirm. All information from their previous open state + // is retained. + historicalChannelBucket = []byte("historical-chan-bucket") + + // chanInfoKey can be accessed within the bucket for a channel + // (identified by its chanPoint). This key stores all the static + // information for a channel which is decided at the end of the + // funding flow. + chanInfoKey = []byte("chan-info-key") + + // localUpfrontShutdownKey can be accessed within the bucket for a + // channel (identified by its chanPoint). This key stores an optional + // upfront shutdown script for the local peer. + localUpfrontShutdownKey = []byte("local-upfront-shutdown-key") + + // remoteUpfrontShutdownKey can be accessed within the bucket for a + // channel (identified by its chanPoint). This key stores an optional + // upfront shutdown script for the remote peer. + remoteUpfrontShutdownKey = []byte("remote-upfront-shutdown-key") + + // frozenChanKey is the key where we store the information for any + // active "frozen" channels. This key is present only in the leaf + // bucket for a given channel. + frozenChanKey = []byte("frozen-chans") + // dataLossCommitPointKey stores the commitment point received from the // remote peer during a channel sync in case we have lost channel state. dataLossCommitPointKey = []byte("data-loss-commit-point-key") ) -// DataLossCommitPointKey returns the key used to store the data-loss commit -// point in a channel bucket. -func DataLossCommitPointKey() []byte { - return dataLossCommitPointKey +// OpenChannelBucketKey returns the top-level open-channel bucket key. +func OpenChannelBucketKey() []byte { + return openChannelBucket } -// PutChannelDataLossCommitPoint stores the data-loss commit point in the -// target channel bucket. -func PutChannelDataLossCommitPoint(chanBucket kvdb.RwBucket, - commitPoint *btcec.PublicKey) error { +// OutpointBucketKey returns the top-level outpoint index bucket key. +func OutpointBucketKey() []byte { + return outpointBucket +} - return chanBucket.Put( - dataLossCommitPointKey, commitPoint.SerializeCompressed(), - ) +// ChanIDBucketKey returns the top-level channel ID index bucket key. +func ChanIDBucketKey() []byte { + return chanIDBucket } -// FetchChannelDataLossCommitPoint retrieves the data-loss commit point from the -// target channel bucket. -func FetchChannelDataLossCommitPoint( - chanBucket kvdb.RBucket) (*btcec.PublicKey, error) { +// HistoricalChannelBucketKey returns the top-level historical channel bucket +// key. +func HistoricalChannelBucketKey() []byte { + return historicalChannelBucket +} - bs := chanBucket.Get(dataLossCommitPointKey) - if bs == nil { - return nil, ErrNoCommitPoint - } +// ChanInfoKey returns the channel-bucket key for static channel information. +func ChanInfoKey() []byte { + return chanInfoKey +} - var b [btcec.PubKeyBytesLenCompressed]byte - r := bytes.NewReader(bs) - if _, err := io.ReadFull(r, b[:]); err != nil { - return nil, err - } +// LocalUpfrontShutdownKey returns the channel-bucket key for the local upfront +// shutdown script. +func LocalUpfrontShutdownKey() []byte { + return localUpfrontShutdownKey +} - return btcec.ParsePubKey(b[:]) +// RemoteUpfrontShutdownKey returns the channel-bucket key for the remote +// upfront shutdown script. +func RemoteUpfrontShutdownKey() []byte { + return remoteUpfrontShutdownKey +} + +// FrozenChanKey returns the key used to store a channel's thaw height. +func FrozenChanKey() []byte { + return frozenChanKey +} + +// DataLossCommitPointKey returns the key used to store the data-loss commit +// point in a channel bucket. +func DataLossCommitPointKey() []byte { + return dataLossCommitPointKey } const ( @@ -283,6 +349,35 @@ func FetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey, return chanBucket, nil } +// FetchThawHeight fetches a channel's thaw height from the channel bucket. +func FetchThawHeight(chanBucket kvdb.RBucket) (uint32, error) { + var height uint32 + + heightBytes := chanBucket.Get(frozenChanKey) + heightReader := bytes.NewReader(heightBytes) + + if err := binary.Read(heightReader, byteOrder, &height); err != nil { + return 0, err + } + + return height, nil +} + +// StoreThawHeight stores a channel's thaw height in the channel bucket. +func StoreThawHeight(chanBucket kvdb.RwBucket, height uint32) error { + var heightBuf bytes.Buffer + if err := binary.Write(&heightBuf, byteOrder, height); err != nil { + return err + } + + return chanBucket.Put(frozenChanKey, heightBuf.Bytes()) +} + +// DeleteThawHeight deletes a channel's thaw height from the channel bucket. +func DeleteThawHeight(chanBucket kvdb.RwBucket) error { + return chanBucket.Delete(frozenChanKey) +} + // FullSyncOpenChannel syncs the contents of an OpenChannel while re-using an // existing database transaction. func FullSyncOpenChannel(tx kvdb.RwTx, c *OpenChannel) error { @@ -871,6 +966,35 @@ func ApplyChannelStatus(backend kvdb.Backend, channel *OpenChannel, return PutChanStatus(backend, channel, status) } +// PutChannelDataLossCommitPoint stores the data-loss commit point in the +// target channel bucket. +func PutChannelDataLossCommitPoint(chanBucket kvdb.RwBucket, + commitPoint *btcec.PublicKey) error { + + return chanBucket.Put( + dataLossCommitPointKey, commitPoint.SerializeCompressed(), + ) +} + +// FetchChannelDataLossCommitPoint retrieves the data-loss commit point from the +// target channel bucket. +func FetchChannelDataLossCommitPoint( + chanBucket kvdb.RBucket) (*btcec.PublicKey, error) { + + bs := chanBucket.Get(dataLossCommitPointKey) + if bs == nil { + return nil, ErrNoCommitPoint + } + + var b [btcec.PubKeyBytesLenCompressed]byte + r := bytes.NewReader(bs) + if _, err := io.ReadFull(r, b[:]); err != nil { + return nil, err + } + + return btcec.ParsePubKey(b[:]) +} + // MarkChannelDataLoss marks the channel as local-data-loss and stores the // commit point needed if the remote force closes. func MarkChannelDataLoss(backend kvdb.Backend, channel *OpenChannel, diff --git a/chanstate/kv_thaw_height.go b/chanstate/kv_thaw_height.go deleted file mode 100644 index 64b4991534..0000000000 --- a/chanstate/kv_thaw_height.go +++ /dev/null @@ -1,49 +0,0 @@ -package chanstate - -import ( - "bytes" - "encoding/binary" - - "github.com/lightningnetwork/lnd/kvdb" -) - -var ( - // frozenChanKey is the key where we store the information for any - // active "frozen" channels. This key is present only in the leaf - // bucket for a given channel. - frozenChanKey = []byte("frozen-chans") -) - -// FrozenChanKey returns the key used to store a channel's thaw height. -func FrozenChanKey() []byte { - return frozenChanKey -} - -// FetchThawHeight fetches a channel's thaw height from the channel bucket. -func FetchThawHeight(chanBucket kvdb.RBucket) (uint32, error) { - var height uint32 - - heightBytes := chanBucket.Get(frozenChanKey) - heightReader := bytes.NewReader(heightBytes) - - if err := binary.Read(heightReader, byteOrder, &height); err != nil { - return 0, err - } - - return height, nil -} - -// StoreThawHeight stores a channel's thaw height in the channel bucket. -func StoreThawHeight(chanBucket kvdb.RwBucket, height uint32) error { - var heightBuf bytes.Buffer - if err := binary.Write(&heightBuf, byteOrder, height); err != nil { - return err - } - - return chanBucket.Put(frozenChanKey, heightBuf.Bytes()) -} - -// DeleteThawHeight deletes a channel's thaw height from the channel bucket. -func DeleteThawHeight(chanBucket kvdb.RwBucket) error { - return chanBucket.Delete(frozenChanKey) -} From aa6713576bf1d0f8ea9061532792ebf6275bf2e7 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 10:41:00 -0300 Subject: [PATCH 49/61] chanstate: move channel setup kv store Move the KV-backed ChannelSetupStore implementation onto the chanstate KVStore. This includes both channel-opening state and initial forwarding policy persistence, which together make up the setup store facet. Keep the channeldb ChannelStateDB methods as compatibility wrappers so callers can continue using the old package while the concrete KV store implementation moves by facet. --- channeldb/db.go | 28 ++----- channeldb/forwarding_policy.go | 86 +------------------ chanstate/kv_channel_setup.go | 149 ++++++++++++++++++++++++++++++++- chanstate/kv_store.go | 19 +++++ 4 files changed, 178 insertions(+), 104 deletions(-) create mode 100644 chanstate/kv_store.go diff --git a/channeldb/db.go b/channeldb/db.go index 6a90cd6222..527f9e219d 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -412,6 +412,7 @@ func CreateWithBackend(backend kvdb.Backend, modifiers ...OptionModifier) (*DB, linkNodeDB: &LinkNodeDB{ backend: backend, }, + kvStore: chanstate.NewKVStore(backend), backend: backend, tombstoneClosedChannels: opts.tombstoneClosedChannels, }, @@ -544,6 +545,10 @@ type ChannelStateDB struct { // database. This may be a real backend or a cache middleware. backend kvdb.Backend + // kvStore is the chanstate-owned KV implementation. ChannelStateDB + // keeps compatibility wrappers while callers still import channeldb. + kvStore *chanstate.KVStore + // tombstoneClosedChannels is set by OptionTombstoneClosedChannels. // When true, CloseChannel skips deleting nested per-channel state and // relies on the outpointBucket flip to outpointClosed as the @@ -1793,11 +1798,7 @@ func (c *ChannelStateDB) AbandonChannel(chanPoint *wire.OutPoint, func (c *ChannelStateDB) SaveChannelOpeningState(outPoint, serializedState []byte) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - return chanstate.SaveChannelOpeningState( - tx, outPoint, serializedState, - ) - }, func() {}) + return c.kvStore.SaveChannelOpeningState(outPoint, serializedState) } // GetChannelOpeningState fetches the serialized channel state for the provided @@ -1806,25 +1807,12 @@ func (c *ChannelStateDB) SaveChannelOpeningState(outPoint, func (c *ChannelStateDB) GetChannelOpeningState(outPoint []byte) ([]byte, error) { - var serializedState []byte - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - var err error - serializedState, err = chanstate.GetChannelOpeningState( - tx, outPoint, - ) - - return err - }, func() { - serializedState = nil - }) - return serializedState, err + return c.kvStore.GetChannelOpeningState(outPoint) } // DeleteChannelOpeningState removes any state for outPoint from the database. func (c *ChannelStateDB) DeleteChannelOpeningState(outPoint []byte) error { - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - return chanstate.DeleteChannelOpeningState(tx, outPoint) - }, func() {}) + return c.kvStore.DeleteChannelOpeningState(outPoint) } // syncVersions function is used for safe db version synchronization. It diff --git a/channeldb/forwarding_policy.go b/channeldb/forwarding_policy.go index 2df2e308f8..3bcfbda002 100644 --- a/channeldb/forwarding_policy.go +++ b/channeldb/forwarding_policy.go @@ -2,44 +2,15 @@ package channeldb import ( "github.com/lightningnetwork/lnd/graph/db/models" - "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" ) -var ( - // initialChannelForwardingPolicyBucket is the database bucket used to - // store the forwarding policy for each permanent channel that is - // currently in the process of being opened. - initialChannelForwardingPolicyBucket = []byte( - "initialChannelFwdingPolicy", - ) -) - // SaveInitialForwardingPolicy saves the serialized forwarding policy for the // provided permanent channel id to the initialChannelForwardingPolicyBucket. func (c *ChannelStateDB) SaveInitialForwardingPolicy(chanID lnwire.ChannelID, forwardingPolicy *models.ForwardingPolicy) error { - chanIDCopy := make([]byte, 32) - copy(chanIDCopy, chanID[:]) - - scratch := make([]byte, 36) - byteOrder.PutUint64(scratch[:8], uint64(forwardingPolicy.MinHTLCOut)) - byteOrder.PutUint64(scratch[8:16], uint64(forwardingPolicy.MaxHTLC)) - byteOrder.PutUint64(scratch[16:24], uint64(forwardingPolicy.BaseFee)) - byteOrder.PutUint64(scratch[24:32], uint64(forwardingPolicy.FeeRate)) - byteOrder.PutUint32(scratch[32:], forwardingPolicy.TimeLockDelta) - - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - bucket, err := tx.CreateTopLevelBucket( - initialChannelForwardingPolicyBucket, - ) - if err != nil { - return err - } - - return bucket.Put(chanIDCopy, scratch) - }, func() {}) + return c.kvStore.SaveInitialForwardingPolicy(chanID, forwardingPolicy) } // GetInitialForwardingPolicy fetches the serialized forwarding policy for the @@ -48,46 +19,7 @@ func (c *ChannelStateDB) SaveInitialForwardingPolicy(chanID lnwire.ChannelID, func (c *ChannelStateDB) GetInitialForwardingPolicy( chanID lnwire.ChannelID) (*models.ForwardingPolicy, error) { - chanIDCopy := make([]byte, 32) - copy(chanIDCopy, chanID[:]) - - var forwardingPolicy *models.ForwardingPolicy - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - bucket := tx.ReadBucket(initialChannelForwardingPolicyBucket) - if bucket == nil { - // If the bucket does not exist, it means we - // never added a channel fees to the db, so - // return ErrChannelNotFound. - return ErrChannelNotFound - } - - stateBytes := bucket.Get(chanIDCopy) - if stateBytes == nil { - return ErrChannelNotFound - } - - forwardingPolicy = &models.ForwardingPolicy{ - MinHTLCOut: lnwire.MilliSatoshi( - byteOrder.Uint64(stateBytes[:8]), - ), - MaxHTLC: lnwire.MilliSatoshi( - byteOrder.Uint64(stateBytes[8:16]), - ), - BaseFee: lnwire.MilliSatoshi( - byteOrder.Uint64(stateBytes[16:24]), - ), - FeeRate: lnwire.MilliSatoshi( - byteOrder.Uint64(stateBytes[24:32]), - ), - TimeLockDelta: byteOrder.Uint32(stateBytes[32:36]), - } - - return nil - }, func() { - forwardingPolicy = nil - }) - - return forwardingPolicy, err + return c.kvStore.GetInitialForwardingPolicy(chanID) } // DeleteInitialForwardingPolicy removes the forwarding policy for a given @@ -95,17 +27,5 @@ func (c *ChannelStateDB) GetInitialForwardingPolicy( func (c *ChannelStateDB) DeleteInitialForwardingPolicy( chanID lnwire.ChannelID) error { - chanIDCopy := make([]byte, 32) - copy(chanIDCopy, chanID[:]) - - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - bucket := tx.ReadWriteBucket( - initialChannelForwardingPolicyBucket, - ) - if bucket == nil { - return ErrChannelNotFound - } - - return bucket.Delete(chanIDCopy) - }, func() {}) + return c.kvStore.DeleteInitialForwardingPolicy(chanID) } diff --git a/chanstate/kv_channel_setup.go b/chanstate/kv_channel_setup.go index 555444b9ae..fc06232f9c 100644 --- a/chanstate/kv_channel_setup.go +++ b/chanstate/kv_channel_setup.go @@ -1,12 +1,23 @@ package chanstate -import "github.com/lightningnetwork/lnd/kvdb" +import ( + graphmodels "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lnwire" +) var ( // channelOpeningStateBucket is the database bucket used to store the // channelOpeningState for each channel that is currently in the process // of being opened. channelOpeningStateBucket = []byte("channelOpeningState") + + // initialChannelForwardingPolicyBucket is the database bucket used to + // store the forwarding policy for each permanent channel that is + // currently in the process of being opened. + initialChannelForwardingPolicyBucket = []byte( + "initialChannelFwdingPolicy", + ) ) // ChannelOpeningStateBucketKey returns the top-level bucket key used to store @@ -15,6 +26,12 @@ func ChannelOpeningStateBucketKey() []byte { return channelOpeningStateBucket } +// InitialChannelForwardingPolicyBucketKey returns the top-level bucket key used +// to store initial channel forwarding policies. +func InitialChannelForwardingPolicyBucketKey() []byte { + return initialChannelForwardingPolicyBucket +} + // SaveChannelOpeningState saves the serialized channel state for the provided // chanPoint to the channelOpeningStateBucket. func SaveChannelOpeningState(tx kvdb.RwTx, outPoint, @@ -28,6 +45,16 @@ func SaveChannelOpeningState(tx kvdb.RwTx, outPoint, return bucket.Put(outPoint, serializedState) } +// SaveChannelOpeningState saves the serialized channel state for the provided +// chanPoint to the channelOpeningStateBucket. +func (s *KVStore) SaveChannelOpeningState(outPoint, + serializedState []byte) error { + + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { + return SaveChannelOpeningState(tx, outPoint, serializedState) + }, func() {}) +} + // GetChannelOpeningState fetches the serialized channel state for the provided // outPoint from the database, or returns ErrChannelNotFound if the channel is // not found. @@ -50,6 +77,24 @@ func GetChannelOpeningState(tx kvdb.RTx, outPoint []byte) ([]byte, error) { return serializedState, nil } +// GetChannelOpeningState fetches the serialized channel state for the provided +// outPoint from the database, or returns ErrChannelNotFound if the channel is +// not found. +func (s *KVStore) GetChannelOpeningState(outPoint []byte) ([]byte, error) { + var serializedState []byte + + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + var err error + serializedState, err = GetChannelOpeningState(tx, outPoint) + + return err + }, func() { + serializedState = nil + }) + + return serializedState, err +} + // DeleteChannelOpeningState removes any state for outPoint from the database. func DeleteChannelOpeningState(tx kvdb.RwTx, outPoint []byte) error { bucket := tx.ReadWriteBucket(channelOpeningStateBucket) @@ -59,3 +104,105 @@ func DeleteChannelOpeningState(tx kvdb.RwTx, outPoint []byte) error { return bucket.Delete(outPoint) } + +// DeleteChannelOpeningState removes any state for outPoint from the database. +func (s *KVStore) DeleteChannelOpeningState(outPoint []byte) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { + return DeleteChannelOpeningState(tx, outPoint) + }, func() {}) +} + +// SaveInitialForwardingPolicy saves the serialized forwarding policy for the +// provided permanent channel id to the initialChannelForwardingPolicyBucket. +func (s *KVStore) SaveInitialForwardingPolicy(chanID lnwire.ChannelID, + forwardingPolicy *graphmodels.ForwardingPolicy) error { + + chanIDCopy := make([]byte, 32) + copy(chanIDCopy, chanID[:]) + + scratch := make([]byte, 36) + byteOrder.PutUint64(scratch[:8], uint64(forwardingPolicy.MinHTLCOut)) + byteOrder.PutUint64(scratch[8:16], uint64(forwardingPolicy.MaxHTLC)) + byteOrder.PutUint64(scratch[16:24], uint64(forwardingPolicy.BaseFee)) + byteOrder.PutUint64(scratch[24:32], uint64(forwardingPolicy.FeeRate)) + byteOrder.PutUint32(scratch[32:], forwardingPolicy.TimeLockDelta) + + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { + bucket, err := tx.CreateTopLevelBucket( + initialChannelForwardingPolicyBucket, + ) + if err != nil { + return err + } + + return bucket.Put(chanIDCopy, scratch) + }, func() {}) +} + +// GetInitialForwardingPolicy fetches the serialized forwarding policy for the +// provided channel id from the database, or returns ErrChannelNotFound if +// a forwarding policy for this channel id is not found. +func (s *KVStore) GetInitialForwardingPolicy( + chanID lnwire.ChannelID) (*graphmodels.ForwardingPolicy, error) { + + chanIDCopy := make([]byte, 32) + copy(chanIDCopy, chanID[:]) + + var forwardingPolicy *graphmodels.ForwardingPolicy + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + bucket := tx.ReadBucket(initialChannelForwardingPolicyBucket) + if bucket == nil { + // If the bucket does not exist, it means we + // never added a channel fees to the db, so + // return ErrChannelNotFound. + return ErrChannelNotFound + } + + stateBytes := bucket.Get(chanIDCopy) + if stateBytes == nil { + return ErrChannelNotFound + } + + forwardingPolicy = &graphmodels.ForwardingPolicy{ + MinHTLCOut: lnwire.MilliSatoshi( + byteOrder.Uint64(stateBytes[:8]), + ), + MaxHTLC: lnwire.MilliSatoshi( + byteOrder.Uint64(stateBytes[8:16]), + ), + BaseFee: lnwire.MilliSatoshi( + byteOrder.Uint64(stateBytes[16:24]), + ), + FeeRate: lnwire.MilliSatoshi( + byteOrder.Uint64(stateBytes[24:32]), + ), + TimeLockDelta: byteOrder.Uint32(stateBytes[32:36]), + } + + return nil + }, func() { + forwardingPolicy = nil + }) + + return forwardingPolicy, err +} + +// DeleteInitialForwardingPolicy removes the forwarding policy for a given +// channel from the database. +func (s *KVStore) DeleteInitialForwardingPolicy( + chanID lnwire.ChannelID) error { + + chanIDCopy := make([]byte, 32) + copy(chanIDCopy, chanID[:]) + + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { + bucket := tx.ReadWriteBucket( + initialChannelForwardingPolicyBucket, + ) + if bucket == nil { + return ErrChannelNotFound + } + + return bucket.Delete(chanIDCopy) + }, func() {}) +} diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go new file mode 100644 index 0000000000..bd292c2a4b --- /dev/null +++ b/chanstate/kv_store.go @@ -0,0 +1,19 @@ +package chanstate + +import "github.com/lightningnetwork/lnd/kvdb" + +// KVStore is the KV-backed implementation of the channel-state store facets. +// Store facets are moved onto this type incrementally while channeldb keeps +// compatibility wrappers for callers that still depend on the old package. +type KVStore struct { + backend kvdb.Backend +} + +// NewKVStore creates a KV-backed channel-state store. +func NewKVStore(backend kvdb.Backend) *KVStore { + return &KVStore{ + backend: backend, + } +} + +var _ ChannelSetupStore = (*KVStore)(nil) From d7fb955fb00cc26ea91274c28b58a2e5723c5a9f Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 10:48:14 -0300 Subject: [PATCH 50/61] chanstate: move final htlc kv store Move the KV-backed FinalHTLCStore implementation onto KVStore. The ChannelStateDB methods keep compatibility wrappers while final HTLC lookup and on-chain outcome writes now live with the final HTLC KV store code. Carry the store-final-resolution option into KVStore so the on-chain outcome path keeps the existing opt-in behavior. --- channeldb/db.go | 62 +++----------------------------------- chanstate/kv_final_htlc.go | 61 +++++++++++++++++++++++++++++++++++++ chanstate/kv_store.go | 11 +++++-- 3 files changed, 74 insertions(+), 60 deletions(-) diff --git a/channeldb/db.go b/channeldb/db.go index 527f9e219d..fc26cc7831 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -412,7 +412,9 @@ func CreateWithBackend(backend kvdb.Backend, modifiers ...OptionModifier) (*DB, linkNodeDB: &LinkNodeDB{ backend: backend, }, - kvStore: chanstate.NewKVStore(backend), + kvStore: chanstate.NewKVStore( + backend, opts.storeFinalHtlcResolutions, + ), backend: backend, tombstoneClosedChannels: opts.tombstoneClosedChannels, }, @@ -2069,12 +2071,6 @@ func (c *ChannelStateDB) FetchHistoricalChannel(outPoint *wire.OutPoint) ( return channel, nil } -func fetchFinalHtlcsBucket(tx kvdb.RTx, - chanID lnwire.ShortChannelID) (kvdb.RBucket, error) { - - return chanstate.FetchFinalHtlcsBucket(tx, chanID) -} - var ErrHtlcUnknown = chanstate.ErrHtlcUnknown // LookupFinalHtlc retrieves a final htlc resolution from the database. If the @@ -2082,37 +2078,7 @@ var ErrHtlcUnknown = chanstate.ErrHtlcUnknown func (c *ChannelStateDB) LookupFinalHtlc(chanID lnwire.ShortChannelID, htlcIndex uint64) (*FinalHtlcInfo, error) { - var info *FinalHtlcInfo - - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - finalHtlcsBucket, err := fetchFinalHtlcsBucket( - tx, chanID, - ) - switch { - case errors.Is(err, ErrFinalHtlcsBucketNotFound): - fallthrough - - case errors.Is(err, ErrFinalChannelBucketNotFound): - return ErrHtlcUnknown - - case err != nil: - return fmt.Errorf("cannot fetch final htlcs bucket: %w", - err) - } - - info, err = chanstate.FetchFinalHtlc( - finalHtlcsBucket, htlcIndex, - ) - - return err - }, func() { - info = nil - }) - if err != nil { - return nil, err - } - - return info, nil + return c.kvStore.LookupFinalHtlc(chanID, htlcIndex) } // PutOnchainFinalHtlcOutcome stores the final on-chain outcome of an htlc in @@ -2120,25 +2086,7 @@ func (c *ChannelStateDB) LookupFinalHtlc(chanID lnwire.ShortChannelID, func (c *ChannelStateDB) PutOnchainFinalHtlcOutcome( chanID lnwire.ShortChannelID, htlcID uint64, settled bool) error { - // Skip if the user did not opt in to storing final resolutions. - if !c.parent.storeFinalHtlcResolutions { - return nil - } - - return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - finalHtlcsBucket, err := fetchFinalHtlcsBucketRw(tx, chanID) - if err != nil { - return err - } - - return putFinalHtlc( - finalHtlcsBucket, htlcID, - FinalHtlcInfo{ - Settled: settled, - Offchain: false, - }, - ) - }, func() {}) + return c.kvStore.PutOnchainFinalHtlcOutcome(chanID, htlcID, settled) } // MakeTestInvoiceDB is used to create a test invoice database for testing diff --git a/chanstate/kv_final_htlc.go b/chanstate/kv_final_htlc.go index e3de8007d5..2e838875d1 100644 --- a/chanstate/kv_final_htlc.go +++ b/chanstate/kv_final_htlc.go @@ -2,6 +2,7 @@ package chanstate import ( "errors" + "fmt" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -148,6 +149,66 @@ func FetchFinalHtlc(finalHtlcsBucket kvdb.RBucket, return &info, nil } +// LookupFinalHtlc retrieves a final htlc resolution from the database. If the +// htlc has no final resolution yet, ErrHtlcUnknown is returned. +func (s *KVStore) LookupFinalHtlc(chanID lnwire.ShortChannelID, + htlcIndex uint64) (*FinalHtlcInfo, error) { + + var info *FinalHtlcInfo + + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + finalHtlcsBucket, err := FetchFinalHtlcsBucket(tx, chanID) + switch { + case errors.Is(err, ErrFinalHtlcsBucketNotFound): + fallthrough + + case errors.Is(err, ErrFinalChannelBucketNotFound): + return ErrHtlcUnknown + + case err != nil: + return fmt.Errorf("cannot fetch final htlcs bucket: %w", + err) + } + + info, err = FetchFinalHtlc(finalHtlcsBucket, htlcIndex) + + return err + }, func() { + info = nil + }) + if err != nil { + return nil, err + } + + return info, nil +} + +// PutOnchainFinalHtlcOutcome stores the final on-chain outcome of an htlc in +// the database. +func (s *KVStore) PutOnchainFinalHtlcOutcome(chanID lnwire.ShortChannelID, + htlcID uint64, settled bool) error { + + // Skip if the user did not opt in to storing final resolutions. + if !s.storeFinalHtlcResolutions { + return nil + } + + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { + finalHtlcsBucket, err := FetchFinalHtlcsBucketRw(tx, chanID) + if err != nil { + return err + } + + return PutFinalHtlc( + finalHtlcsBucket, htlcID, + FinalHtlcInfo{ + Settled: settled, + Offchain: false, + }, + ) + }, func() {}) +} + // ProcessFinalHtlc stores a final htlc outcome in the database if signaled via // the supplied log update. An in-memory htlcs map is updated too. func ProcessFinalHtlc(finalHtlcsBucket kvdb.RwBucket, upd LogUpdate, diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index bd292c2a4b..c15fb2ccdb 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -6,14 +6,19 @@ import "github.com/lightningnetwork/lnd/kvdb" // Store facets are moved onto this type incrementally while channeldb keeps // compatibility wrappers for callers that still depend on the old package. type KVStore struct { - backend kvdb.Backend + backend kvdb.Backend + storeFinalHtlcResolutions bool } // NewKVStore creates a KV-backed channel-state store. -func NewKVStore(backend kvdb.Backend) *KVStore { +func NewKVStore(backend kvdb.Backend, + storeFinalHtlcResolutions bool) *KVStore { + return &KVStore{ - backend: backend, + backend: backend, + storeFinalHtlcResolutions: storeFinalHtlcResolutions, } } var _ ChannelSetupStore = (*KVStore)(nil) +var _ FinalHTLCStore = (*KVStore)(nil) From d73d1a40198d3f07b4c3046152b6f879530425e6 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 10:51:57 -0300 Subject: [PATCH 51/61] chanstate: move fwd package kv store Move the KV-backed OpenChannelFwdPkgStore implementation onto KVStore. ChannelStateDB keeps the existing wrapper methods, but now delegates through the channelstate KV store instead of passing its backend into package-level helpers. This keeps forwarding package persistence grouped with the existing kv_forwarding_package code while removing another direct backend use from the channeldb compatibility layer. --- channeldb/channel.go | 10 +++++----- chanstate/kv_forwarding_package.go | 20 ++++++++++---------- chanstate/kv_store.go | 1 + 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index e5bf64b5ed..ffe93a1081 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -587,7 +587,7 @@ func putFinalHtlc(finalHtlcsBucket kvdb.RwBucket, id uint64, func (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, error) { - return cstate.LoadFwdPkgs(c.backend, channel) + return c.kvStore.LoadFwdPkgs(channel) } // AckAddHtlcs updates the AckAddFilter containing any of the provided AddRefs @@ -598,7 +598,7 @@ func (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, addRefs ...AddRef) error { - return cstate.AckAddHtlcs(c.backend, channel, addRefs...) + return c.kvStore.AckAddHtlcs(channel, addRefs...) } // AckSettleFails updates the SettleFailFilter containing any of the provided @@ -608,7 +608,7 @@ func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, func (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, settleFailRefs ...SettleFailRef) error { - return cstate.AckSettleFails(c.backend, channel, settleFailRefs...) + return c.kvStore.AckSettleFails(channel, settleFailRefs...) } // SetFwdFilter atomically sets the forwarding filter for the forwarding package @@ -616,7 +616,7 @@ func (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, fwdFilter *PkgFilter) error { - return cstate.SetFwdFilter(c.backend, channel, height, fwdFilter) + return c.kvStore.SetFwdFilter(channel, height, fwdFilter) } // RemoveFwdPkgs atomically removes forwarding packages specified by the remote @@ -629,7 +629,7 @@ func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, heights ...uint64) error { - return cstate.RemoveFwdPkgs(c.backend, channel, heights...) + return c.kvStore.RemoveFwdPkgs(channel, heights...) } // revocationLogTailCommitHeight returns the commit height at the end of the diff --git a/chanstate/kv_forwarding_package.go b/chanstate/kv_forwarding_package.go index 93538b8ed7..6cba8384c5 100644 --- a/chanstate/kv_forwarding_package.go +++ b/chanstate/kv_forwarding_package.go @@ -261,11 +261,11 @@ func newChannelPackager(channel *OpenChannel) *ChannelPackager { // LoadFwdPkgs scans the forwarding log for any packages that haven't been // processed, and returns their deserialized log updates in map indexed by the // remote commitment height at which the updates were locked in. -func LoadFwdPkgs(backend kvdb.Backend, channel *OpenChannel) ([]*FwdPkg, +func (s *KVStore) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, error) { var fwdPkgs []*FwdPkg - if err := kvdb.View(backend, func(tx kvdb.RTx) error { + if err := kvdb.View(s.backend, func(tx kvdb.RTx) error { var err error fwdPkgs, err = newChannelPackager(channel).LoadFwdPkgs(tx) return err @@ -282,10 +282,10 @@ func LoadFwdPkgs(backend kvdb.Backend, channel *OpenChannel) ([]*FwdPkg, // indicating that a response to this Add has been committed to the remote // party. Doing so will prevent these Add HTLCs from being reforwarded // internally. -func AckAddHtlcs(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) AckAddHtlcs(channel *OpenChannel, addRefs ...AddRef) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { return newChannelPackager(channel).AckAddHtlcs(tx, addRefs...) }, func() {}) } @@ -294,10 +294,10 @@ func AckAddHtlcs(backend kvdb.Backend, channel *OpenChannel, // SettleFailRefs, indicating that the response has been delivered to the // incoming link, corresponding to a particular AddRef. Doing so will prevent // the responses from being retransmitted internally. -func AckSettleFails(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) AckSettleFails(channel *OpenChannel, settleFailRefs ...SettleFailRef) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { return newChannelPackager(channel).AckSettleFails( tx, settleFailRefs..., ) @@ -306,10 +306,10 @@ func AckSettleFails(backend kvdb.Backend, channel *OpenChannel, // SetFwdFilter atomically sets the forwarding filter for the forwarding package // identified by `height`. -func SetFwdFilter(backend kvdb.Backend, channel *OpenChannel, height uint64, +func (s *KVStore) SetFwdFilter(channel *OpenChannel, height uint64, fwdFilter *PkgFilter) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { return newChannelPackager(channel).SetFwdFilter( tx, height, fwdFilter, ) @@ -321,10 +321,10 @@ func SetFwdFilter(backend kvdb.Backend, channel *OpenChannel, height uint64, // the later packages won't be removed. // // NOTE: This method should only be called on packages marked FwdStateCompleted. -func RemoveFwdPkgs(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) RemoveFwdPkgs(channel *OpenChannel, heights ...uint64) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { packager := newChannelPackager(channel) for _, height := range heights { diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index c15fb2ccdb..4a4409a052 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -22,3 +22,4 @@ func NewKVStore(backend kvdb.Backend, var _ ChannelSetupStore = (*KVStore)(nil) var _ FinalHTLCStore = (*KVStore)(nil) +var _ OpenChannelFwdPkgStore = (*KVStore)(nil) From fd88dba84fea58bbb4baeee052c4f9e1f3371071 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 10:55:16 -0300 Subject: [PATCH 52/61] chanstate: move shutdown kv store Move the KV-backed OpenChannelShutdownStore methods onto KVStore. ChannelStateDB keeps the existing compatibility methods, but shutdown state now flows through the channelstate KV store rather than backend helper functions. The bucket-level shutdown codec helper stays in kv_shutdown because it is still the local serialization primitive used by the store method. --- channeldb/channel.go | 4 ++-- chanstate/kv_shutdown.go | 11 ++++++----- chanstate/kv_store.go | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index ffe93a1081..623c62f82a 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -338,7 +338,7 @@ var ( func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, info *ShutdownInfo) error { - return cstate.StoreChannelShutdownInfo(c.backend, channel, info) + return c.kvStore.StoreChannelShutdownInfo(channel, info) } // FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the target @@ -346,7 +346,7 @@ func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, func (c *ChannelStateDB) FetchChannelShutdownInfo( channel *OpenChannel) (fn.Option[ShutdownInfo], error) { - return cstate.FetchShutdownInfo(c.backend, channel) + return c.kvStore.FetchChannelShutdownInfo(channel) } // MarkChannelCommitmentBroadcasted marks the channel as having a commitment diff --git a/chanstate/kv_shutdown.go b/chanstate/kv_shutdown.go index 46b2b8cb72..89a3304dd1 100644 --- a/chanstate/kv_shutdown.go +++ b/chanstate/kv_shutdown.go @@ -53,10 +53,10 @@ func FetchChannelShutdownInfo(chanBucket kvdb.RBucket) ( } // StoreChannelShutdownInfo persists the ShutdownInfo for the target channel. -func StoreChannelShutdownInfo(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) StoreChannelShutdownInfo(channel *OpenChannel, info *ShutdownInfo) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -69,12 +69,13 @@ func StoreChannelShutdownInfo(backend kvdb.Backend, channel *OpenChannel, }, func() {}) } -// FetchShutdownInfo fetches the persisted ShutdownInfo for the target channel. -func FetchShutdownInfo(backend kvdb.Backend, +// FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the target +// channel. +func (s *KVStore) FetchChannelShutdownInfo( channel *OpenChannel) (fn.Option[ShutdownInfo], error) { var shutdownInfo *ShutdownInfo - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index 4a4409a052..5fdfa8cb53 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -23,3 +23,4 @@ func NewKVStore(backend kvdb.Backend, var _ ChannelSetupStore = (*KVStore)(nil) var _ FinalHTLCStore = (*KVStore)(nil) var _ OpenChannelFwdPkgStore = (*KVStore)(nil) +var _ OpenChannelShutdownStore = (*KVStore)(nil) From c686d906d582a6ab03cc68c3b945b9dfa14f8de2 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 10:58:59 -0300 Subject: [PATCH 53/61] chanstate: move close tx kv store Move the KV-backed OpenChannelCloseTxStore implementation onto KVStore. ChannelStateDB keeps compatibility wrappers for marking and fetching broadcast close transactions while the KV logic now lives in kv_close_tx. Keep the generic close transaction fetch helper private to KVStore and expose only the domain methods required by the channelstate interface. --- channeldb/channel.go | 20 +++++--------------- chanstate/kv_close_tx.go | 38 +++++++++++++++++++++++++++----------- chanstate/kv_store.go | 1 + 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 623c62f82a..96afffcc9b 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -355,8 +355,8 @@ func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { - return cstate.MarkChannelCommitmentBroadcasted( - c.backend, channel, closeTx, closer, + return c.kvStore.MarkChannelCommitmentBroadcasted( + channel, closeTx, closer, ) } @@ -365,9 +365,7 @@ func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( func (c *ChannelStateDB) MarkChannelCoopBroadcasted(channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { - return cstate.MarkChannelCoopBroadcasted( - c.backend, channel, closeTx, closer, - ) + return c.kvStore.MarkChannelCoopBroadcasted(channel, closeTx, closer) } // FetchChannelBroadcastedCommitment fetches the stored unilateral closing @@ -375,7 +373,7 @@ func (c *ChannelStateDB) MarkChannelCoopBroadcasted(channel *OpenChannel, func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( channel *OpenChannel) (*wire.MsgTx, error) { - return c.getClosingTx(channel, cstate.ForceCloseTxKey()) + return c.kvStore.FetchChannelBroadcastedCommitment(channel) } // FetchChannelBroadcastedCooperative fetches the stored cooperative closing @@ -383,15 +381,7 @@ func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( func (c *ChannelStateDB) FetchChannelBroadcastedCooperative( channel *OpenChannel) (*wire.MsgTx, error) { - return c.getClosingTx(channel, cstate.CoopCloseTxKey()) -} - -// getClosingTx returns the stored closing transaction for key. The caller -// should use either the force or coop closing keys. -func (c *ChannelStateDB) getClosingTx(channel *OpenChannel, - key []byte) (*wire.MsgTx, error) { - - return cstate.FetchClosingTx(c.backend, channel, key) + return c.kvStore.FetchChannelBroadcastedCooperative(channel) } // ApplyChannelStatus adds the target status to the channel's persisted status diff --git a/chanstate/kv_close_tx.go b/chanstate/kv_close_tx.go index 60b34887d8..782956cd04 100644 --- a/chanstate/kv_close_tx.go +++ b/chanstate/kv_close_tx.go @@ -66,24 +66,24 @@ func FetchChannelCloseTx(chanBucket kvdb.RBucket, // MarkChannelCommitmentBroadcasted marks the channel as having a commitment // transaction broadcast. -func MarkChannelCommitmentBroadcasted(backend kvdb.Backend, +func (s *KVStore) MarkChannelCommitmentBroadcasted( channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { - return markBroadcasted( - backend, channel, ChanStatusCommitBroadcasted, forceCloseTxKey, + return s.markBroadcasted( + channel, ChanStatusCommitBroadcasted, forceCloseTxKey, closeTx, closer, ) } // MarkChannelCoopBroadcasted marks the channel as having a cooperative close // transaction broadcast. -func MarkChannelCoopBroadcasted(backend kvdb.Backend, +func (s *KVStore) MarkChannelCoopBroadcasted( channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { - return markBroadcasted( - backend, channel, ChanStatusCoopBroadcasted, coopCloseTxKey, + return s.markBroadcasted( + channel, ChanStatusCoopBroadcasted, coopCloseTxKey, closeTx, closer, ) } @@ -91,7 +91,7 @@ func MarkChannelCoopBroadcasted(backend kvdb.Backend, // markBroadcasted modifies the channel status and inserts a close transaction // under the requested key, which should specify either a coop or force close. // It adds a status which indicates the party that initiated the channel close. -func markBroadcasted(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) markBroadcasted(channel *OpenChannel, status ChannelStatus, key []byte, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { @@ -115,17 +115,33 @@ func markBroadcasted(backend kvdb.Backend, channel *OpenChannel, status |= ChanStatusRemoteCloseInitiator } - return PutChanStatus(backend, channel, status, putClosingTx) + return PutChanStatus(s.backend, channel, status, putClosingTx) } -// FetchClosingTx returns the stored closing transaction for key. The caller +// FetchChannelBroadcastedCommitment fetches the stored unilateral closing +// transaction. +func (s *KVStore) FetchChannelBroadcastedCommitment( + channel *OpenChannel) (*wire.MsgTx, error) { + + return s.fetchClosingTx(channel, forceCloseTxKey) +} + +// FetchChannelBroadcastedCooperative fetches the stored cooperative closing +// transaction. +func (s *KVStore) FetchChannelBroadcastedCooperative( + channel *OpenChannel) (*wire.MsgTx, error) { + + return s.fetchClosingTx(channel, coopCloseTxKey) +} + +// fetchClosingTx returns the stored closing transaction for key. The caller // should use either the force or coop closing keys. -func FetchClosingTx(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) fetchClosingTx(channel *OpenChannel, key []byte) (*wire.MsgTx, error) { var closeTx *wire.MsgTx - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index 5fdfa8cb53..5181fc7fc2 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -24,3 +24,4 @@ var _ ChannelSetupStore = (*KVStore)(nil) var _ FinalHTLCStore = (*KVStore)(nil) var _ OpenChannelFwdPkgStore = (*KVStore)(nil) var _ OpenChannelShutdownStore = (*KVStore)(nil) +var _ OpenChannelCloseTxStore = (*KVStore)(nil) From f1d0b6b49294e8242b035f89860bd5b965d13b27 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 11:02:25 -0300 Subject: [PATCH 54/61] chanstate: move status kv store Move the KV-backed OpenChannelStatusStore implementation onto KVStore. ChannelStateDB keeps compatibility wrappers while status updates, data-loss commit point lookup, and borked marking now flow through the channelstate KV store. Keep the status write helper private to KVStore so related KV store facets can share the same status mutation path without passing raw backends around. --- channeldb/channel.go | 10 +++++----- chanstate/kv_close_tx.go | 2 +- chanstate/kv_open_channel.go | 33 +++++++++++++++++++-------------- chanstate/kv_store.go | 1 + 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 96afffcc9b..ba8183d989 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -308,7 +308,7 @@ func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, commitPoint *btcec.PublicKey) error { - return cstate.MarkChannelDataLoss(c.backend, channel, commitPoint) + return c.kvStore.MarkChannelDataLoss(channel, commitPoint) } // FetchChannelDataLossCommitPoint retrieves the commit point stored when the @@ -316,12 +316,12 @@ func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, func (c *ChannelStateDB) FetchChannelDataLossCommitPoint( channel *OpenChannel) (*btcec.PublicKey, error) { - return cstate.FetchDataLossCommitPoint(c.backend, channel) + return c.kvStore.FetchChannelDataLossCommitPoint(channel) } // MarkChannelBorked marks the channel as irreconcilable. func (c *ChannelStateDB) MarkChannelBorked(channel *OpenChannel) error { - return c.ApplyChannelStatus(channel, ChanStatusBorked) + return c.kvStore.MarkChannelBorked(channel) } var ( @@ -389,7 +389,7 @@ func (c *ChannelStateDB) FetchChannelBroadcastedCooperative( func (c *ChannelStateDB) ApplyChannelStatus(channel *OpenChannel, status ChannelStatus) error { - return cstate.ApplyChannelStatus(c.backend, channel, status) + return c.kvStore.ApplyChannelStatus(channel, status) } // ClearChannelStatus clears the target status from the channel's persisted @@ -397,7 +397,7 @@ func (c *ChannelStateDB) ApplyChannelStatus(channel *OpenChannel, func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, status ChannelStatus) error { - return cstate.ClearChannelStatus(c.backend, channel, status) + return c.kvStore.ClearChannelStatus(channel, status) } // fetchOpenChannel retrieves, and deserializes (including decrypting diff --git a/chanstate/kv_close_tx.go b/chanstate/kv_close_tx.go index 782956cd04..1c8b05f83e 100644 --- a/chanstate/kv_close_tx.go +++ b/chanstate/kv_close_tx.go @@ -115,7 +115,7 @@ func (s *KVStore) markBroadcasted(channel *OpenChannel, status |= ChanStatusRemoteCloseInitiator } - return PutChanStatus(s.backend, channel, status, putClosingTx) + return s.putChanStatus(channel, status, putClosingTx) } // FetchChannelBroadcastedCommitment fetches the stored unilateral closing diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index 44ca0498fb..e0188f1dd0 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -960,10 +960,10 @@ func MarkChannelScidAliasNegotiated(backend kvdb.Backend, // ApplyChannelStatus adds the target status to the channel's persisted status // bit field. -func ApplyChannelStatus(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) ApplyChannelStatus(channel *OpenChannel, status ChannelStatus) error { - return PutChanStatus(backend, channel, status) + return s.putChanStatus(channel, status) } // PutChannelDataLossCommitPoint stores the data-loss commit point in the @@ -997,26 +997,26 @@ func FetchChannelDataLossCommitPoint( // MarkChannelDataLoss marks the channel as local-data-loss and stores the // commit point needed if the remote force closes. -func MarkChannelDataLoss(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) MarkChannelDataLoss(channel *OpenChannel, commitPoint *btcec.PublicKey) error { putCommitPoint := func(chanBucket kvdb.RwBucket) error { return PutChannelDataLossCommitPoint(chanBucket, commitPoint) } - return PutChanStatus( - backend, channel, ChanStatusLocalDataLoss, putCommitPoint, + return s.putChanStatus( + channel, ChanStatusLocalDataLoss, putCommitPoint, ) } -// FetchDataLossCommitPoint retrieves the commit point stored when the channel -// was marked as local-data-loss. -func FetchDataLossCommitPoint(backend kvdb.Backend, +// FetchChannelDataLossCommitPoint retrieves the commit point stored when the +// channel was marked as local-data-loss. +func (s *KVStore) FetchChannelDataLossCommitPoint( channel *OpenChannel) (*btcec.PublicKey, error) { var commitPoint *btcec.PublicKey - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -1045,13 +1045,18 @@ func FetchDataLossCommitPoint(backend kvdb.Backend, return commitPoint, nil } -// PutChanStatus appends the given status to the channel. fs is an optional +// MarkChannelBorked marks the channel as irreconcilable. +func (s *KVStore) MarkChannelBorked(channel *OpenChannel) error { + return s.ApplyChannelStatus(channel, ChanStatusBorked) +} + +// putChanStatus appends the given status to the channel. fs is an optional // list of closures that are given the chanBucket in order to atomically add // extra information together with the new status. -func PutChanStatus(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) putChanStatus(channel *OpenChannel, status ChannelStatus, fs ...func(kvdb.RwBucket) error) error { - if err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + if err := kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -1099,10 +1104,10 @@ func PutChanStatus(backend kvdb.Backend, channel *OpenChannel, // ClearChannelStatus clears the target status from the channel's persisted // status bit field. -func ClearChannelStatus(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) ClearChannelStatus(channel *OpenChannel, status ChannelStatus) error { - if err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + if err := kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index 5181fc7fc2..74a93cdd38 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -25,3 +25,4 @@ var _ FinalHTLCStore = (*KVStore)(nil) var _ OpenChannelFwdPkgStore = (*KVStore)(nil) var _ OpenChannelShutdownStore = (*KVStore)(nil) var _ OpenChannelCloseTxStore = (*KVStore)(nil) +var _ OpenChannelStatusStore = (*KVStore)(nil) From 2483beaabf6ee0e38b72951116f77a0418c48ee3 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 11:05:38 -0300 Subject: [PATCH 55/61] chanstate: move lifecycle kv methods Move the backend-local open channel lifecycle mutations onto KVStore. ChannelStateDB keeps compatibility wrappers for refreshing channel state and marking confirmation, open, real scid, and scid-alias negotiation state. Leave SyncPendingChannel in channeldb for now because that path still creates link-node records, and LinkNodeDB ownership remains in channeldb for this refactor. --- channeldb/channel.go | 14 ++++++-------- chanstate/kv_open_channel.go | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index ba8183d989..e078cf5517 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -253,7 +253,7 @@ const ( // RefreshChannel updates the in-memory channel state using the latest state // observed on disk. func (c *ChannelStateDB) RefreshChannel(channel *OpenChannel) error { - return cstate.RefreshChannel(c.backend, channel) + return c.kvStore.RefreshChannel(channel) } func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, @@ -267,7 +267,7 @@ func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, height uint32) error { - return cstate.MarkChannelConfirmationHeight(c.backend, channel, height) + return c.kvStore.MarkChannelConfirmationHeight(channel, height) } // MarkChannelCloseConfirmationHeight updates the channel's close confirmation @@ -275,9 +275,7 @@ func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, func (c *ChannelStateDB) MarkChannelCloseConfirmationHeight( channel *OpenChannel, height fn.Option[uint32]) error { - return cstate.MarkChannelCloseConfirmationHeight( - c.backend, channel, height, - ) + return c.kvStore.MarkChannelCloseConfirmationHeight(channel, height) } // MarkChannelOpen marks a channel as fully open given a locator that uniquely @@ -285,14 +283,14 @@ func (c *ChannelStateDB) MarkChannelCloseConfirmationHeight( func (c *ChannelStateDB) MarkChannelOpen(channel *OpenChannel, openLoc lnwire.ShortChannelID) error { - return cstate.MarkChannelOpen(c.backend, channel, openLoc) + return c.kvStore.MarkChannelOpen(channel, openLoc) } // MarkChannelRealScid marks the zero-conf channel's confirmed ShortChannelID. func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, realScid lnwire.ShortChannelID) error { - return cstate.MarkChannelRealScid(c.backend, channel, realScid) + return c.kvStore.MarkChannelRealScid(channel, realScid) } // MarkChannelScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in the @@ -300,7 +298,7 @@ func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( channel *OpenChannel) error { - return cstate.MarkChannelScidAliasNegotiated(c.backend, channel) + return c.kvStore.MarkChannelScidAliasNegotiated(channel) } // MarkChannelDataLoss marks the channel as local-data-loss and stores the diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index e0188f1dd0..19c30c25cc 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -788,8 +788,8 @@ func SyncPendingOpenChannel(tx kvdb.RwTx, channel *OpenChannel, // RefreshChannel updates the in-memory channel state using the latest state // observed on disk. -func RefreshChannel(backend kvdb.Backend, channel *OpenChannel) error { - return kvdb.View(backend, func(tx kvdb.RTx) error { +func (s *KVStore) RefreshChannel(channel *OpenChannel) error { + return kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -825,10 +825,10 @@ func RefreshChannel(backend kvdb.Backend, channel *OpenChannel) error { // MarkChannelConfirmationHeight updates the channel's confirmation height once // the channel opening transaction receives one confirmation. -func MarkChannelConfirmationHeight(backend kvdb.Backend, - channel *OpenChannel, height uint32) error { +func (s *KVStore) MarkChannelConfirmationHeight(channel *OpenChannel, + height uint32) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -852,10 +852,10 @@ func MarkChannelConfirmationHeight(backend kvdb.Backend, // MarkChannelCloseConfirmationHeight updates the channel's close confirmation // height when the closing transaction is first detected in a block. -func MarkChannelCloseConfirmationHeight(backend kvdb.Backend, +func (s *KVStore) MarkChannelCloseConfirmationHeight( channel *OpenChannel, height fn.Option[uint32]) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -879,10 +879,10 @@ func MarkChannelCloseConfirmationHeight(backend kvdb.Backend, // MarkChannelOpen marks a channel as fully open given a locator that uniquely // describes its location within the chain. -func MarkChannelOpen(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) MarkChannelOpen(channel *OpenChannel, openLoc lnwire.ShortChannelID) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -906,10 +906,10 @@ func MarkChannelOpen(backend kvdb.Backend, channel *OpenChannel, } // MarkChannelRealScid marks the zero-conf channel's confirmed ShortChannelID. -func MarkChannelRealScid(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) MarkChannelRealScid(channel *OpenChannel, realScid lnwire.ShortChannelID) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -933,10 +933,10 @@ func MarkChannelRealScid(backend kvdb.Backend, channel *OpenChannel, // MarkChannelScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in the // database. -func MarkChannelScidAliasNegotiated(backend kvdb.Backend, +func (s *KVStore) MarkChannelScidAliasNegotiated( channel *OpenChannel) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, From c1a36442fbe49d58ddd758cdd843ff95fc128409 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 11:09:24 -0300 Subject: [PATCH 56/61] chanstate: move commitment kv store Move the KV-backed open channel commitment store methods onto KVStore. ChannelStateDB keeps compatibility wrappers for commitment updates, pending commit diffs, revocation queries, and previous-state lookups. Carry the no-rev-log-amount-data option into KVStore alongside the existing final HTLC option so revocation log and final-resolution writes preserve their previous configuration behavior. --- channeldb/channel.go | 28 +++++++++----------- channeldb/db.go | 1 + chanstate/kv_commitment.go | 48 +++++++++++++++++----------------- chanstate/kv_revocation_log.go | 4 +-- chanstate/kv_store.go | 5 +++- 5 files changed, 44 insertions(+), 42 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index e078cf5517..dcfd507ede 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -460,9 +460,8 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, newCommitment *ChannelCommitment, unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) { - return cstate.UpdateChannelCommitment( - c.backend, channel, newCommitment, unsignedAckedUpdates, - c.parent.storeFinalHtlcResolutions, + return c.kvStore.UpdateChannelCommitment( + channel, newCommitment, unsignedAckedUpdates, ) } @@ -510,7 +509,7 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, diff *CommitDiff) error { - return cstate.AppendRemoteCommitChain(c.backend, channel, diff) + return c.kvStore.AppendRemoteCommitChain(channel, diff) } // RemoteCommitChainTip returns the "tip" of the current remote commitment @@ -518,7 +517,7 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( *CommitDiff, error) { - return cstate.RemoteCommitChainTip(c.backend, channel) + return c.kvStore.RemoteCommitChainTip(channel) } // UnsignedAckedUpdates retrieves the persisted unsigned acked remote log @@ -526,7 +525,7 @@ func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( []LogUpdate, error) { - return cstate.UnsignedAckedUpdates(c.backend, channel) + return c.kvStore.UnsignedAckedUpdates(channel) } // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log @@ -534,7 +533,7 @@ func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( []LogUpdate, error) { - return cstate.RemoteUnsignedLocalUpdates(c.backend, channel) + return c.kvStore.RemoteUnsignedLocalUpdates(channel) } // InsertNextRevocation inserts the next commitment point into the persisted @@ -542,7 +541,7 @@ func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( func (c *ChannelStateDB) InsertNextRevocation(channel *OpenChannel, revKey *btcec.PublicKey) error { - return cstate.InsertNextRevocation(c.backend, channel, revKey) + return c.kvStore.InsertNextRevocation(channel, revKey) } // AdvanceCommitChainTail records the new state transition within the @@ -552,9 +551,8 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, fwdPkg *FwdPkg, updates []LogUpdate, ourOutputIndex, theirOutputIndex uint32) error { - return cstate.AdvanceCommitChainTail( - c.backend, channel, fwdPkg, updates, ourOutputIndex, - theirOutputIndex, c.parent.noRevLogAmtData, + return c.kvStore.AdvanceCommitChainTail( + channel, fwdPkg, updates, ourOutputIndex, theirOutputIndex, ) } @@ -636,7 +634,7 @@ func (c *ChannelStateDB) revocationLogTailCommitHeight( func (c *ChannelStateDB) CommitmentHeight(channel *OpenChannel) ( uint64, error) { - return cstate.CommitmentHeight(c.backend, channel) + return c.kvStore.CommitmentHeight(channel) } // FindPreviousState scans through the append-only log in an attempt to recover @@ -647,7 +645,7 @@ func (c *ChannelStateDB) CommitmentHeight(channel *OpenChannel) ( func (c *ChannelStateDB) FindPreviousState(channel *OpenChannel, updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { - return cstate.FindPreviousState(c.backend, channel, updateNum) + return c.kvStore.FindPreviousState(channel, updateNum) } // ClosureType is an enum like structure that details exactly how a channel was @@ -709,7 +707,7 @@ type ChannelSnapshot = cstate.ChannelSnapshot func (c *ChannelStateDB) LatestCommitments(channel *OpenChannel) ( *ChannelCommitment, *ChannelCommitment, error) { - return cstate.LatestCommitments(c.backend, channel) + return c.kvStore.LatestCommitments(channel) } // RemoteRevocationStore returns the most up to date commitment version of the @@ -719,7 +717,7 @@ func (c *ChannelStateDB) LatestCommitments(channel *OpenChannel) ( func (c *ChannelStateDB) RemoteRevocationStore(channel *OpenChannel) ( shachain.Store, error) { - return cstate.RemoteRevocationStore(c.backend, channel) + return c.kvStore.RemoteRevocationStore(channel) } func serializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error { diff --git a/channeldb/db.go b/channeldb/db.go index fc26cc7831..a9fed991fd 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -414,6 +414,7 @@ func CreateWithBackend(backend kvdb.Backend, modifiers ...OptionModifier) (*DB, }, kvStore: chanstate.NewKVStore( backend, opts.storeFinalHtlcResolutions, + opts.NoRevLogAmtData, ), backend: backend, tombstoneClosedChannels: opts.tombstoneClosedChannels, diff --git a/chanstate/kv_commitment.go b/chanstate/kv_commitment.go index c4e638e05c..8fa94e62a4 100644 --- a/chanstate/kv_commitment.go +++ b/chanstate/kv_commitment.go @@ -495,11 +495,11 @@ func DeleteOpenChannel(chanBucket kvdb.RwBucket) error { // RemoteCommitChainTip returns the "tip" of the current remote commitment // chain. -func RemoteCommitChainTip(backend kvdb.Backend, +func (s *KVStore) RemoteCommitChainTip( channel *OpenChannel) (*CommitDiff, error) { var cd *CommitDiff - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -541,11 +541,11 @@ func RemoteCommitChainTip(backend kvdb.Backend, // UnsignedAckedUpdates retrieves the persisted unsigned acked remote log // updates that still need to be signed for. -func UnsignedAckedUpdates(backend kvdb.Backend, +func (s *KVStore) UnsignedAckedUpdates( channel *OpenChannel) ([]LogUpdate, error) { var updates []LogUpdate - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -582,11 +582,11 @@ func UnsignedAckedUpdates(backend kvdb.Backend, // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log // updates that the remote still needs to sign for. -func RemoteUnsignedLocalUpdates(backend kvdb.Backend, +func (s *KVStore) RemoteUnsignedLocalUpdates( channel *OpenChannel) ([]LogUpdate, error) { var updates []LogUpdate - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -623,12 +623,12 @@ func RemoteUnsignedLocalUpdates(backend kvdb.Backend, // InsertNextRevocation inserts the next commitment point into the persisted // channel state. -func InsertNextRevocation(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) InsertNextRevocation(channel *OpenChannel, revKey *btcec.PublicKey) error { channel.RemoteNextRevocation = revKey - err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + err := kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -647,14 +647,14 @@ func InsertNextRevocation(backend kvdb.Backend, channel *OpenChannel, } // UpdateChannelCommitment updates the local commitment state. -func UpdateChannelCommitment(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) UpdateChannelCommitment(channel *OpenChannel, newCommitment *ChannelCommitment, - unsignedAckedUpdates []LogUpdate, storeFinalHtlcResolutions bool) ( + unsignedAckedUpdates []LogUpdate) ( map[uint64]bool, error) { var finalHtlcs = make(map[uint64]bool) - err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + err := kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -731,7 +731,7 @@ func UpdateChannelCommitment(backend kvdb.Backend, channel *OpenChannel, // Get the bucket where settled htlcs are recorded if the user // opted in to storing this information. var finalHtlcsBucket kvdb.RwBucket - if storeFinalHtlcResolutions { + if s.storeFinalHtlcResolutions { bucket, err := FetchFinalHtlcsBucketRw( tx, channel.ShortChannelID, ) @@ -787,10 +787,10 @@ func UpdateChannelCommitment(backend kvdb.Backend, channel *OpenChannel, // AppendRemoteCommitChain appends a new CommitDiff to the remote party's // commitment chain. -func AppendRemoteCommitChain(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) AppendRemoteCommitChain(channel *OpenChannel, diff *CommitDiff) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { // First, we'll grab the writable bucket where this channel's // data resides. chanBucket, err := FetchChanBucketRw( @@ -863,13 +863,13 @@ func AppendRemoteCommitChain(backend kvdb.Backend, channel *OpenChannel, // AdvanceCommitChainTail records the new state transition within the // revocation log and promotes the pending remote commitment to the current // remote commitment. -func AdvanceCommitChainTail(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) AdvanceCommitChainTail(channel *OpenChannel, fwdPkg *FwdPkg, updates []LogUpdate, ourOutputIndex, - theirOutputIndex uint32, noRevLogAmtData bool) error { + theirOutputIndex uint32) error { var newRemoteCommit *ChannelCommitment - err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + err := kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chanBucket, err := FetchChanBucketRw( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -932,7 +932,7 @@ func AdvanceCommitChainTail(backend kvdb.Backend, channel *OpenChannel, // revoked (prior) state to the revocation log. err = PutRevocationLog( logBucket, &channel.RemoteCommitment, ourOutputIndex, - theirOutputIndex, noRevLogAmtData, + theirOutputIndex, s.noRevLogAmtData, ) if err != nil { return err @@ -1026,11 +1026,11 @@ func AdvanceCommitChainTail(backend kvdb.Backend, channel *OpenChannel, // This value is always monotonically increasing. This method is provided in // order to allow multiple instances of a particular open channel to obtain a // consistent view of the number of channel updates to date. -func CommitmentHeight(backend kvdb.Backend, channel *OpenChannel) ( +func (s *KVStore) CommitmentHeight(channel *OpenChannel) ( uint64, error) { var height uint64 - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { // Get the bucket dedicated to storing the metadata for open // channels. chanBucket, err := FetchChanBucket( @@ -1063,10 +1063,10 @@ func CommitmentHeight(backend kvdb.Backend, channel *OpenChannel) ( // remote party. These commitments are read from disk to ensure that only the // latest fully committed state is returned. The first commitment returned is // the local commitment, and the second returned is the remote commitment. -func LatestCommitments(backend kvdb.Backend, channel *OpenChannel) ( +func (s *KVStore) LatestCommitments(channel *OpenChannel) ( *ChannelCommitment, *ChannelCommitment, error) { - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, @@ -1088,10 +1088,10 @@ func LatestCommitments(backend kvdb.Backend, channel *OpenChannel) ( // revocation storage tree for the remote party. This method can be used when // acting on a possible contract breach to ensure, that the caller has the most // up to date information required to deliver justice. -func RemoteRevocationStore(backend kvdb.Backend, +func (s *KVStore) RemoteRevocationStore( channel *OpenChannel) (shachain.Store, error) { - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, diff --git a/chanstate/kv_revocation_log.go b/chanstate/kv_revocation_log.go index c2e84869ed..e38d833519 100644 --- a/chanstate/kv_revocation_log.go +++ b/chanstate/kv_revocation_log.go @@ -299,13 +299,13 @@ func RevocationLogTailCommitHeight(backend kvdb.Backend, // intended to be used for obtaining the relevant data needed to claim all // funds rightfully spendable in the case of an on-chain broadcast of the // commitment transaction. -func FindPreviousState(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) FindPreviousState(channel *OpenChannel, updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { commit := &ChannelCommitment{} rl := &RevocationLog{} - err := kvdb.View(backend, func(tx kvdb.RTx) error { + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index 74a93cdd38..38bb3d7501 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -7,15 +7,17 @@ import "github.com/lightningnetwork/lnd/kvdb" // compatibility wrappers for callers that still depend on the old package. type KVStore struct { backend kvdb.Backend + noRevLogAmtData bool storeFinalHtlcResolutions bool } // NewKVStore creates a KV-backed channel-state store. func NewKVStore(backend kvdb.Backend, - storeFinalHtlcResolutions bool) *KVStore { + storeFinalHtlcResolutions, noRevLogAmtData bool) *KVStore { return &KVStore{ backend: backend, + noRevLogAmtData: noRevLogAmtData, storeFinalHtlcResolutions: storeFinalHtlcResolutions, } } @@ -26,3 +28,4 @@ var _ OpenChannelFwdPkgStore = (*KVStore)(nil) var _ OpenChannelShutdownStore = (*KVStore)(nil) var _ OpenChannelCloseTxStore = (*KVStore)(nil) var _ OpenChannelStatusStore = (*KVStore)(nil) +var _ OpenChannelCommitmentStore = (*KVStore)(nil) From f70f2800918ef79e973632e879690394b829a10d Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 11:12:33 -0300 Subject: [PATCH 57/61] chanstate: move revocation tail helper Move the KV-backed revocation log tail helper onto KVStore. This removes the last direct backend call from the channeldb open channel compatibility methods while keeping the test-facing helper in place. --- channeldb/channel.go | 2 +- chanstate/kv_revocation_log.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index dcfd507ede..ad76cfd9bb 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -623,7 +623,7 @@ func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, func (c *ChannelStateDB) revocationLogTailCommitHeight( channel *OpenChannel) (uint64, error) { - return cstate.RevocationLogTailCommitHeight(c.backend, channel) + return c.kvStore.RevocationLogTailCommitHeight(channel) } // CommitmentHeight returns the current commitment height. The commitment diff --git a/chanstate/kv_revocation_log.go b/chanstate/kv_revocation_log.go index e38d833519..76ac41005f 100644 --- a/chanstate/kv_revocation_log.go +++ b/chanstate/kv_revocation_log.go @@ -253,7 +253,7 @@ func DeleteLogBucket(chanBucket kvdb.RwBucket) error { // RevocationLogTailCommitHeight returns the commit height at the end of the // revocation log. -func RevocationLogTailCommitHeight(backend kvdb.Backend, +func (s *KVStore) RevocationLogTailCommitHeight( channel *OpenChannel) (uint64, error) { var height uint64 @@ -264,7 +264,7 @@ func RevocationLogTailCommitHeight(backend kvdb.Backend, return height, nil } - if err := kvdb.View(backend, func(tx kvdb.RTx) error { + if err := kvdb.View(s.backend, func(tx kvdb.RTx) error { chanBucket, err := FetchChanBucket( tx, channel.IdentityPub, &channel.FundingOutpoint, channel.ChainHash, From dd1fede57172c2e1f9412ca5641353e410c14f75 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 11:24:32 -0300 Subject: [PATCH 58/61] chanstate: move closed summary kv methods Move closed-channel summary reads and CloseChannel onto KVStore. ChannelStateDB keeps compatibility wrappers, while the KV code for close summary lookup and close archiving now lives with kv_close_summary. Carry the tombstone-close option into KVStore so the existing close strategy selection remains unchanged. Leave MarkChanFullyClosed and AbandonChannel in channeldb because they still coordinate link-node pruning and cross-store behavior. --- channeldb/channel.go | 5 +- channeldb/db.go | 117 ++-------------------- channeldb/error.go | 6 +- chanstate/errors.go | 9 ++ chanstate/kv_close_summary.go | 183 ++++++++++++++++++++++++++++++---- chanstate/kv_store.go | 5 +- 6 files changed, 186 insertions(+), 139 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index ad76cfd9bb..def7f6b00f 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -691,10 +691,7 @@ type ChannelCloseSummary = cstate.ChannelCloseSummary func (c *ChannelStateDB) CloseChannel(channel *OpenChannel, summary *ChannelCloseSummary, statuses ...ChannelStatus) error { - return cstate.CloseChannel( - c.backend, channel, summary, c.tombstoneClosedChannels, - statuses..., - ) + return c.kvStore.CloseChannel(channel, summary, statuses...) } // ChannelSnapshot is a frozen snapshot of the current channel state. diff --git a/channeldb/db.go b/channeldb/db.go index a9fed991fd..84742b6cd1 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -413,8 +413,10 @@ func CreateWithBackend(backend kvdb.Backend, modifiers ...OptionModifier) (*DB, backend: backend, }, kvStore: chanstate.NewKVStore( - backend, opts.storeFinalHtlcResolutions, + backend, + opts.storeFinalHtlcResolutions, opts.NoRevLogAmtData, + opts.tombstoneClosedChannels, ), backend: backend, tombstoneClosedChannels: opts.tombstoneClosedChannels, @@ -1304,78 +1306,15 @@ func fetchChannels(c *ChannelStateDB, filters ...fetchChannelsFilter) ( func (c *ChannelStateDB) FetchClosedChannels(pendingOnly bool) ( []*ChannelCloseSummary, error) { - var chanSummaries []*ChannelCloseSummary - - if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - closeBucket := tx.ReadBucket(closedChannelBucket) - if closeBucket == nil { - return ErrNoClosedChannels - } - - return closeBucket.ForEach(func(chanID []byte, summaryBytes []byte) error { - summaryReader := bytes.NewReader(summaryBytes) - chanSummary, err := deserializeCloseChannelSummary(summaryReader) - if err != nil { - return err - } - - // If the query specified to only include pending - // channels, then we'll skip any channels which aren't - // currently pending. - if !chanSummary.IsPending && pendingOnly { - return nil - } - - chanSummaries = append(chanSummaries, chanSummary) - return nil - }) - }, func() { - chanSummaries = nil - }); err != nil { - return nil, err - } - - return chanSummaries, nil + return c.kvStore.FetchClosedChannels(pendingOnly) } -// ErrClosedChannelNotFound signals that a closed channel could not be found in -// the channeldb. -var ErrClosedChannelNotFound = errors.New("unable to find closed channel summary") - // FetchClosedChannel queries for a channel close summary using the channel // point of the channel in question. func (c *ChannelStateDB) FetchClosedChannel(chanID *wire.OutPoint) ( *ChannelCloseSummary, error) { - var chanSummary *ChannelCloseSummary - if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - closeBucket := tx.ReadBucket(closedChannelBucket) - if closeBucket == nil { - return ErrClosedChannelNotFound - } - - var b bytes.Buffer - var err error - if err = graphdb.WriteOutpoint(&b, chanID); err != nil { - return err - } - - summaryBytes := closeBucket.Get(b.Bytes()) - if summaryBytes == nil { - return ErrClosedChannelNotFound - } - - summaryReader := bytes.NewReader(summaryBytes) - chanSummary, err = deserializeCloseChannelSummary(summaryReader) - - return err - }, func() { - chanSummary = nil - }); err != nil { - return nil, err - } - - return chanSummary, nil + return c.kvStore.FetchClosedChannel(chanID) } // FetchClosedChannelForID queries for a channel close summary using the @@ -1383,51 +1322,7 @@ func (c *ChannelStateDB) FetchClosedChannel(chanID *wire.OutPoint) ( func (c *ChannelStateDB) FetchClosedChannelForID(cid lnwire.ChannelID) ( *ChannelCloseSummary, error) { - var chanSummary *ChannelCloseSummary - if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - closeBucket := tx.ReadBucket(closedChannelBucket) - if closeBucket == nil { - return ErrClosedChannelNotFound - } - - // The first 30 bytes of the channel ID and outpoint will be - // equal. - cursor := closeBucket.ReadCursor() - op, c := cursor.Seek(cid[:30]) - - // We scan over all possible candidates for this channel ID. - for ; op != nil && bytes.Compare(cid[:30], op[:30]) <= 0; op, c = cursor.Next() { - var outPoint wire.OutPoint - err := graphdb.ReadOutpoint( - bytes.NewReader(op), &outPoint, - ) - if err != nil { - return err - } - - // If the found outpoint does not correspond to this - // channel ID, we continue. - if !cid.IsChanPoint(&outPoint) { - continue - } - - // Deserialize the close summary and return. - r := bytes.NewReader(c) - chanSummary, err = deserializeCloseChannelSummary(r) - if err != nil { - return err - } - - return nil - } - return ErrClosedChannelNotFound - }, func() { - chanSummary = nil - }); err != nil { - return nil, err - } - - return chanSummary, nil + return c.kvStore.FetchClosedChannelForID(cid) } // MarkChanFullyClosed marks a channel as fully closed within the database. A diff --git a/channeldb/error.go b/channeldb/error.go index ad50964a01..84e9b52634 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -46,7 +46,11 @@ var ( // ErrNoClosedChannels is returned when a node is queries for all the // channels it has closed, but it hasn't yet closed any channels. - ErrNoClosedChannels = fmt.Errorf("no channel have been closed yet") + ErrNoClosedChannels = cstate.ErrNoClosedChannels + + // ErrClosedChannelNotFound signals that a closed channel could not be + // found in the channeldb. + ErrClosedChannelNotFound = cstate.ErrClosedChannelNotFound // ErrNoForwardingEvents is returned in the case that a query fails due // to the log not having any recorded events. diff --git a/chanstate/errors.go b/chanstate/errors.go index b33d05cfe5..29526a1dc2 100644 --- a/chanstate/errors.go +++ b/chanstate/errors.go @@ -41,6 +41,15 @@ var ( // channels within the database. ErrNoActiveChannels = fmt.Errorf("no active channels exist") + // ErrNoClosedChannels is returned when a node is queries for all the + // channels it has closed, but it hasn't yet closed any channels. + ErrNoClosedChannels = fmt.Errorf("no channel have been closed yet") + + // ErrClosedChannelNotFound signals that a closed channel could not be + // found in the channel state store. + ErrClosedChannelNotFound = errors.New("unable to find closed " + + "channel summary") + // ErrNoPastDeltas is returned when the channel delta bucket hasn't // been created. ErrNoPastDeltas = fmt.Errorf("channel has no recorded deltas") diff --git a/chanstate/kv_close_summary.go b/chanstate/kv_close_summary.go index 0db52ad4d5..a9a4a5f03e 100644 --- a/chanstate/kv_close_summary.go +++ b/chanstate/kv_close_summary.go @@ -5,6 +5,7 @@ import ( "errors" "io" + "github.com/btcsuite/btcd/wire/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -46,21 +47,19 @@ func PutChannelCloseSummary(tx kvdb.RwTx, chanID []byte, // CloseChannel closes the supplied channel via the selected close strategy. On // synchronous backends the channel's nested state — the revocation log, the -// per-channel forwarding-package bucket, and the chanBucket itself — is deleted -// inline. On tombstone-enabled backends none of the bulk state is touched; the -// outpointBucket flip to outpointClosed signals that the channel is logically -// closed. -func CloseChannel(backend kvdb.Backend, channel *OpenChannel, - summary *ChannelCloseSummary, tombstoneClosedChannels bool, +// per-channel forwarding-package bucket, and the chanBucket itself — is +// deleted inline. On tombstone-enabled backends none of the bulk state is +// touched; the outpointBucket flip to outpointClosed signals that the channel +// is logically closed. +func (s *KVStore) CloseChannel(channel *OpenChannel, + summary *ChannelCloseSummary, statuses ...ChannelStatus) error { - if tombstoneClosedChannels { - return CloseChannelTombstone( - backend, channel, summary, statuses..., - ) + if s.tombstoneClosedChannels { + return s.closeChannelTombstone(channel, summary, statuses...) } - return CloseChannelSync(backend, channel, summary, statuses...) + return s.closeChannelSync(channel, summary, statuses...) } // LocateOpenChannel performs the open-channel-bucket descent for a CloseChannel @@ -151,15 +150,15 @@ func ArchiveClosedChannel(tx kvdb.RwTx, chanKey []byte, return PutChannelCloseSummary(tx, chanKey, summary, chanState) } -// CloseChannelSync performs the historical synchronous close path: in a single +// closeChannelSync performs the historical synchronous close path: in a single // write transaction it wipes the forwarding-package state, deletes the channel // bucket and its nested revocation log entries, updates the outpoint index, and // archives the close summary. It is used by backends where nested-bucket // deletion is cheap (bbolt, etcd). -func CloseChannelSync(backend kvdb.Backend, channel *OpenChannel, +func (s *KVStore) closeChannelSync(channel *OpenChannel, summary *ChannelCloseSummary, statuses ...ChannelStatus) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { chainBucket, chanBucket, chanKey, err := LocateOpenChannel( tx, channel, ) @@ -209,16 +208,16 @@ func CloseChannelSync(backend kvdb.Backend, channel *OpenChannel, }, func() {}) } -// CloseChannelTombstone performs the tombstone close path used by KV-over-SQL -// backends. The channel's per-channel state is left intact — touching it would -// trigger the cascading nested-bucket delete this path exists to avoid — and -// the outpointBucket flip from outpointOpen to outpointClosed serves as the -// authoritative closed-channel marker. The disk space is reclaimed wholesale by -// the upcoming native-SQL channel-state migration. -func CloseChannelTombstone(backend kvdb.Backend, channel *OpenChannel, +// closeChannelTombstone performs the tombstone close path used by KV-over-SQL +// backends. The channel's per-channel state is left intact — touching it +// would trigger the cascading nested-bucket delete this path exists to avoid +// — and the outpointBucket flip from outpointOpen to outpointClosed serves as +// the authoritative closed-channel marker. The disk space is reclaimed +// wholesale by the upcoming native-SQL channel-state migration. +func (s *KVStore) closeChannelTombstone(channel *OpenChannel, summary *ChannelCloseSummary, statuses ...ChannelStatus) error { - return kvdb.Update(backend, func(tx kvdb.RwTx) error { + return kvdb.Update(s.backend, func(tx kvdb.RwTx) error { _, chanBucket, chanKey, err := LocateOpenChannel(tx, channel) if err != nil { return err @@ -241,6 +240,146 @@ func CloseChannelTombstone(backend kvdb.Backend, channel *OpenChannel, }, func() {}) } +// FetchClosedChannels attempts to fetch all closed channels from the database. +// The pendingOnly bool toggles if channels that aren't yet fully closed should +// be returned in the response or not. When a channel was cooperatively closed, +// it becomes fully closed after a single confirmation. When a channel was +// forcibly closed, it will become fully closed after _all_ the pending funds +// (if any) have been swept. +func (s *KVStore) FetchClosedChannels(pendingOnly bool) ( + []*ChannelCloseSummary, error) { + + var chanSummaries []*ChannelCloseSummary + + if err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + closeBucket := tx.ReadBucket(closedChannelBucket) + if closeBucket == nil { + return ErrNoClosedChannels + } + + return closeBucket.ForEach(func(chanID []byte, + summaryBytes []byte) error { + + summaryReader := bytes.NewReader(summaryBytes) + chanSummary, err := DeserializeCloseChannelSummary( + summaryReader, + ) + if err != nil { + return err + } + + // If the query specified to only include pending + // channels, then we'll skip any channels which aren't + // currently pending. + if !chanSummary.IsPending && pendingOnly { + return nil + } + + chanSummaries = append(chanSummaries, chanSummary) + + return nil + }) + }, func() { + chanSummaries = nil + }); err != nil { + return nil, err + } + + return chanSummaries, nil +} + +// FetchClosedChannel queries for a channel close summary using the channel +// point of the channel in question. +func (s *KVStore) FetchClosedChannel(chanID *wire.OutPoint) ( + *ChannelCloseSummary, error) { + + var chanSummary *ChannelCloseSummary + if err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + closeBucket := tx.ReadBucket(closedChannelBucket) + if closeBucket == nil { + return ErrClosedChannelNotFound + } + + var b bytes.Buffer + var err error + if err = graphdb.WriteOutpoint(&b, chanID); err != nil { + return err + } + + summaryBytes := closeBucket.Get(b.Bytes()) + if summaryBytes == nil { + return ErrClosedChannelNotFound + } + + summaryReader := bytes.NewReader(summaryBytes) + chanSummary, err = DeserializeCloseChannelSummary( + summaryReader, + ) + + return err + }, func() { + chanSummary = nil + }); err != nil { + return nil, err + } + + return chanSummary, nil +} + +// FetchClosedChannelForID queries for a channel close summary using the +// channel ID of the channel in question. +func (s *KVStore) FetchClosedChannelForID(cid lnwire.ChannelID) ( + *ChannelCloseSummary, error) { + + var chanSummary *ChannelCloseSummary + if err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + closeBucket := tx.ReadBucket(closedChannelBucket) + if closeBucket == nil { + return ErrClosedChannelNotFound + } + + // The first 30 bytes of the channel ID and outpoint will be + // equal. + cursor := closeBucket.ReadCursor() + op, c := cursor.Seek(cid[:30]) + + // We scan over all possible candidates for this channel ID. + for op != nil && bytes.Compare(cid[:30], op[:30]) <= 0 { + var outPoint wire.OutPoint + err := graphdb.ReadOutpoint( + bytes.NewReader(op), &outPoint, + ) + if err != nil { + return err + } + + // If the found outpoint corresponds to this channel ID, + // deserialize the close summary and return. + if cid.IsChanPoint(&outPoint) { + r := bytes.NewReader(c) + cs, err := DeserializeCloseChannelSummary(r) + if err != nil { + return err + } + + chanSummary = cs + + return nil + } + + op, c = cursor.Next() + } + + return ErrClosedChannelNotFound + }, func() { + chanSummary = nil + }); err != nil { + return nil, err + } + + return chanSummary, nil +} + // SerializeChannelCloseSummary serializes a channel close summary. func SerializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error { diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index 38bb3d7501..d27851c2e2 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -9,16 +9,19 @@ type KVStore struct { backend kvdb.Backend noRevLogAmtData bool storeFinalHtlcResolutions bool + tombstoneClosedChannels bool } // NewKVStore creates a KV-backed channel-state store. func NewKVStore(backend kvdb.Backend, - storeFinalHtlcResolutions, noRevLogAmtData bool) *KVStore { + storeFinalHtlcResolutions, noRevLogAmtData, + tombstoneClosedChannels bool) *KVStore { return &KVStore{ backend: backend, noRevLogAmtData: noRevLogAmtData, storeFinalHtlcResolutions: storeFinalHtlcResolutions, + tombstoneClosedChannels: tombstoneClosedChannels, } } From 020cd1ada53133aeb12a976c92c94ac21e4f6e1c Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 11:28:30 -0300 Subject: [PATCH 59/61] chanstate: move historical channel kv store Move historical channel bucket lookup and FetchHistoricalChannel onto KVStore. The channeldb wrapper still attaches itself to channel.Db so existing callers continue to receive a compatibility store reference. Move ErrNoHistoricalBucket into chanstate and keep the channeldb error as an alias, matching the other channel-state errors that have moved. --- channeldb/db.go | 49 ++------------------------------ channeldb/error.go | 3 +- chanstate/errors.go | 5 ++++ chanstate/kv_open_channel.go | 54 ++++++++++++++++++++++++++++++++++++ chanstate/kv_store.go | 1 + 5 files changed, 64 insertions(+), 48 deletions(-) diff --git a/channeldb/db.go b/channeldb/db.go index 84742b6cd1..22d98d8a7f 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -1909,61 +1909,18 @@ func getMigrationsToApply(versions []mandatoryVersion, return migrations, migrationVersions } -// fetchHistoricalChanBucket returns a the channel bucket for a given outpoint -// from the historical channel bucket. If the bucket does not exist, -// ErrNoHistoricalBucket is returned. -func fetchHistoricalChanBucket(tx kvdb.RTx, - outPoint *wire.OutPoint) (kvdb.RBucket, error) { - - // First fetch the top level bucket which stores all data related to - // historically stored channels. - historicalChanBucket := tx.ReadBucket(historicalChannelBucket) - if historicalChanBucket == nil { - return nil, ErrNoHistoricalBucket - } - - // With the bucket for the node and chain fetched, we can now go down - // another level, for the channel itself. - var chanPointBuf bytes.Buffer - if err := graphdb.WriteOutpoint(&chanPointBuf, outPoint); err != nil { - return nil, err - } - chanBucket := historicalChanBucket.NestedReadBucket( - chanPointBuf.Bytes(), - ) - if chanBucket == nil { - return nil, ErrChannelNotFound - } - - return chanBucket, nil -} - // FetchHistoricalChannel fetches open channel data from the historical channel // bucket. func (c *ChannelStateDB) FetchHistoricalChannel(outPoint *wire.OutPoint) ( *OpenChannel, error) { - var channel *OpenChannel - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - chanBucket, err := fetchHistoricalChanBucket(tx, outPoint) - if err != nil { - return err - } - - channel, err = fetchOpenChannel(chanBucket, outPoint) - if err != nil { - return err - } - - channel.Db = c - return nil - }, func() { - channel = nil - }) + channel, err := c.kvStore.FetchHistoricalChannel(outPoint) if err != nil { return nil, err } + channel.Db = c + return channel, nil } diff --git a/channeldb/error.go b/channeldb/error.go index 84e9b52634..3a7e5e2439 100644 --- a/channeldb/error.go +++ b/channeldb/error.go @@ -13,8 +13,7 @@ var ( // ErrNoHistoricalBucket is returned when the historical channel bucket // not been created yet. - ErrNoHistoricalBucket = fmt.Errorf("historical channel bucket has " + - "not yet been created") + ErrNoHistoricalBucket = cstate.ErrNoHistoricalBucket // ErrDBReversion is returned when detecting an attempt to revert to a // prior database version. diff --git a/chanstate/errors.go b/chanstate/errors.go index 29526a1dc2..43cf787112 100644 --- a/chanstate/errors.go +++ b/chanstate/errors.go @@ -41,6 +41,11 @@ var ( // channels within the database. ErrNoActiveChannels = fmt.Errorf("no active channels exist") + // ErrNoHistoricalBucket is returned when the historical channel + // bucket not been created yet. + ErrNoHistoricalBucket = fmt.Errorf("historical channel bucket has " + + "not yet been created") + // ErrNoClosedChannels is returned when a node is queries for all the // channels it has closed, but it hasn't yet closed any channels. ErrNoClosedChannels = fmt.Errorf("no channel have been closed yet") diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index 19c30c25cc..f8bb177eb0 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -786,6 +786,60 @@ func SyncPendingOpenChannel(tx kvdb.RwTx, channel *OpenChannel, return FullSyncOpenChannel(tx, channel) } +// FetchHistoricalChanBucket returns a the channel bucket for a given outpoint +// from the historical channel bucket. If the bucket does not exist, +// ErrNoHistoricalBucket is returned. +func FetchHistoricalChanBucket(tx kvdb.RTx, + outPoint *wire.OutPoint) (kvdb.RBucket, error) { + + // First fetch the top level bucket which stores all data related to + // historically stored channels. + historicalChanBucket := tx.ReadBucket(historicalChannelBucket) + if historicalChanBucket == nil { + return nil, ErrNoHistoricalBucket + } + + // With the bucket for the node and chain fetched, we can now go down + // another level, for the channel itself. + var chanPointBuf bytes.Buffer + if err := graphdb.WriteOutpoint(&chanPointBuf, outPoint); err != nil { + return nil, err + } + chanBucket := historicalChanBucket.NestedReadBucket( + chanPointBuf.Bytes(), + ) + if chanBucket == nil { + return nil, ErrChannelNotFound + } + + return chanBucket, nil +} + +// FetchHistoricalChannel fetches open channel data from the historical channel +// bucket. +func (s *KVStore) FetchHistoricalChannel(outPoint *wire.OutPoint) ( + *OpenChannel, error) { + + var channel *OpenChannel + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + chanBucket, err := FetchHistoricalChanBucket(tx, outPoint) + if err != nil { + return err + } + + channel, err = FetchOpenChannel(chanBucket, outPoint) + + return err + }, func() { + channel = nil + }) + if err != nil { + return nil, err + } + + return channel, nil +} + // RefreshChannel updates the in-memory channel state using the latest state // observed on disk. func (s *KVStore) RefreshChannel(channel *OpenChannel) error { diff --git a/chanstate/kv_store.go b/chanstate/kv_store.go index d27851c2e2..2d1c187a6f 100644 --- a/chanstate/kv_store.go +++ b/chanstate/kv_store.go @@ -32,3 +32,4 @@ var _ OpenChannelShutdownStore = (*KVStore)(nil) var _ OpenChannelCloseTxStore = (*KVStore)(nil) var _ OpenChannelStatusStore = (*KVStore)(nil) var _ OpenChannelCommitmentStore = (*KVStore)(nil) +var _ HistoricalChannelStore = (*KVStore)(nil) From 57f87b2b0fe63e4a0a212b8e7133b36590e5c8dc Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 13:39:08 -0300 Subject: [PATCH 60/61] chanstate: move open channel read kv store Move the KV-backed open channel read and scan methods onto KVStore. ChannelStateDB keeps compatibility wrappers that attach themselves to returned OpenChannel values so existing receiver methods still use the channeldb store. Leave restore and sync paths in channeldb because they still coordinate link-node creation and repair while LinkNodeDB ownership remains outside chanstate. --- channeldb/channel.go | 8 - channeldb/channel_test.go | 4 +- channeldb/db.go | 684 +++------------------------------ channeldb/db_test.go | 100 ++--- chanstate/kv_open_channel.go | 721 +++++++++++++++++++++++++++++++++++ 5 files changed, 804 insertions(+), 713 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index def7f6b00f..57e065a070 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -398,14 +398,6 @@ func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, return c.kvStore.ClearChannelStatus(channel, status) } -// fetchOpenChannel retrieves, and deserializes (including decrypting -// sensitive) the complete channel currently active with the passed nodeID. -func fetchOpenChannel(chanBucket kvdb.RBucket, - chanPoint *wire.OutPoint) (*OpenChannel, error) { - - return cstate.FetchOpenChannel(chanBucket, chanPoint) -} - // SyncPendingChannel writes a pending channel to the store and records the // funding broadcast height. func (c *ChannelStateDB) SyncPendingChannel(channel *OpenChannel, diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 6d149160de..215855e644 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -1521,9 +1521,7 @@ func TestCloseInitiator(t *testing.T) { } // Lookup open channels in the database. - dbChans, err := fetchChannels( - cdb, pendingChannelFilter(false), - ) + dbChans, err := cdb.FetchAllChannels() if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/channeldb/db.go b/channeldb/db.go index 22d98d8a7f..1aa8e7fa8a 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -12,7 +12,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire/v2" - "github.com/btcsuite/btcwallet/walletdb" mig "github.com/lightningnetwork/lnd/channeldb/migration" "github.com/lightningnetwork/lnd/channeldb/migration12" "github.com/lightningnetwork/lnd/channeldb/migration13" @@ -580,16 +579,12 @@ func (c *ChannelStateDB) LinkNodeDB() *LinkNodeDB { func (c *ChannelStateDB) FetchOpenChannels(nodeID *btcec.PublicKey) ( []*OpenChannel, error) { - var channels []*OpenChannel - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - var err error - channels, err = c.fetchOpenChannels(tx, nodeID) - return err - }, func() { - channels = nil - }) + channels, err := c.kvStore.FetchOpenChannels(nodeID) + if err != nil { + return nil, err + } - return channels, err + return c.attachOpenChannelStores(channels), nil } // fetchOpenChannels uses and existing database transaction and returns all @@ -599,115 +594,32 @@ func (c *ChannelStateDB) FetchOpenChannels(nodeID *btcec.PublicKey) ( func (c *ChannelStateDB) fetchOpenChannels(tx kvdb.RTx, nodeID *btcec.PublicKey) ([]*OpenChannel, error) { - // Get the bucket dedicated to storing the metadata for open channels. - openChanBucket := tx.ReadBucket(openChannelBucket) - if openChanBucket == nil { - return nil, nil - } - - // Within this top level bucket, fetch the bucket dedicated to storing - // open channel data specific to the remote node. - pub := nodeID.SerializeCompressed() - nodeChanBucket := openChanBucket.NestedReadBucket(pub) - if nodeChanBucket == nil { - return nil, nil + channels, err := chanstate.FetchOpenChannelsTx(tx, nodeID) + if err != nil { + return nil, err } - // Next, we'll need to go down an additional layer in order to retrieve - // the channels for each chain the node knows of. - var channels []*OpenChannel - err := nodeChanBucket.ForEach(func(chainHash, v []byte) error { - // If there's a value, it's not a bucket so ignore it. - if v != nil { - return nil - } - - // If we've found a valid chainhash bucket, then we'll retrieve - // that so we can extract all the channels. - chainBucket := nodeChanBucket.NestedReadBucket(chainHash) - if chainBucket == nil { - return fmt.Errorf("unable to read bucket for chain=%x", - chainHash[:]) - } - - // Finally, we both of the necessary buckets retrieved, fetch - // all the active channels related to this node. - nodeChannels, err := c.fetchNodeChannels(tx, chainBucket) - if err != nil { - return fmt.Errorf("unable to read channel for "+ - "chain_hash=%x, node_key=%x: %v", - chainHash[:], pub, err) - } - - channels = append(channels, nodeChannels...) - return nil - }) - - return channels, err + return c.attachOpenChannelStores(channels), nil } -// fetchNodeChannels retrieves all active channels from the target chainBucket -// which is under a node's dedicated channel bucket. This function is typically -// used to fetch all the active channels related to a particular node. Channels -// already flipped to outpointClosed in the outpoint index are skipped silently -// — readers see only channels that are still considered open. -func (c *ChannelStateDB) fetchNodeChannels(tx kvdb.RTx, - chainBucket kvdb.RBucket) ([]*OpenChannel, error) { - - var channels []*OpenChannel - - // Hoist the outpoint-bucket lookup so the closed-channel check inside - // the loop is a per-iteration map probe rather than a tx-level bucket - // resolve. - opBucket := tx.ReadBucket(outpointBucket) - - // A node may have channels on several chains, so for each known chain, - // we'll extract all the channels. - err := chainBucket.ForEach(func(chanPoint, v []byte) error { - // If there's a value, it's not a bucket so ignore it. - if v != nil { - return nil - } +func (c *ChannelStateDB) attachOpenChannelStore( + channel *OpenChannel) *OpenChannel { - // Skip already-closed channels. The chanBucket still exists - // on disk on tombstone-enabled backends; the outpoint flip is - // the sole signal that the channel should be treated as - // closed. - isClosed, err := isOutpointClosed(opBucket, chanPoint) - if err != nil { - return err - } - if isClosed { - return nil - } - - // Once we've found a valid channel bucket, we'll extract it - // from the node's chain bucket. - chanBucket := chainBucket.NestedReadBucket(chanPoint) + if channel != nil { + channel.Db = c + } - var outPoint wire.OutPoint - err = graphdb.ReadOutpoint( - bytes.NewReader(chanPoint), &outPoint, - ) - if err != nil { - return err - } - oChannel, err := fetchOpenChannel(chanBucket, &outPoint) - if err != nil { - return fmt.Errorf("unable to read channel data for "+ - "chan_point=%v: %w", outPoint, err) - } - oChannel.Db = c + return channel +} - channels = append(channels, oChannel) +func (c *ChannelStateDB) attachOpenChannelStores( + channels []*OpenChannel) []*OpenChannel { - return nil - }) - if err != nil { - return nil, err + for _, channel := range channels { + c.attachOpenChannelStore(channel) } - return channels, nil + return channels } // FetchChannel attempts to locate a channel specified by the passed channel @@ -715,20 +627,12 @@ func (c *ChannelStateDB) fetchNodeChannels(tx kvdb.RTx, func (c *ChannelStateDB) FetchChannel(chanPoint wire.OutPoint) (*OpenChannel, error) { - var targetChanPoint bytes.Buffer - err := graphdb.WriteOutpoint(&targetChanPoint, &chanPoint) + channel, err := c.kvStore.FetchChannel(chanPoint) if err != nil { return nil, err } - targetChanPointBytes := targetChanPoint.Bytes() - selector := func(chainBkt walletdb.ReadBucket) ([]byte, *wire.OutPoint, - error) { - - return targetChanPointBytes, &chanPoint, nil - } - - return c.channelScanner(nil, selector) + return c.attachOpenChannelStore(channel), nil } // FetchChannelByID attempts to locate a channel specified by the passed channel @@ -736,48 +640,12 @@ func (c *ChannelStateDB) FetchChannel(chanPoint wire.OutPoint) (*OpenChannel, func (c *ChannelStateDB) FetchChannelByID(id lnwire.ChannelID) (*OpenChannel, error) { - selector := func(chainBkt walletdb.ReadBucket) ([]byte, *wire.OutPoint, - error) { - - var ( - targetChanPointBytes []byte - targetChanPoint *wire.OutPoint - - // errChanFound is used to signal that the channel has - // been found so that iteration through the DB buckets - // can stop. - errChanFound = errors.New("channel found") - ) - err := chainBkt.ForEach(func(k, _ []byte) error { - var outPoint wire.OutPoint - err := graphdb.ReadOutpoint( - bytes.NewReader(k), &outPoint, - ) - if err != nil { - return err - } - - chanID := lnwire.NewChanIDFromOutPoint(outPoint) - if chanID != id { - return nil - } - - targetChanPoint = &outPoint - targetChanPointBytes = k - - return errChanFound - }) - if err != nil && !errors.Is(err, errChanFound) { - return nil, nil, err - } - if targetChanPoint == nil { - return nil, nil, ErrChannelNotFound - } - - return targetChanPointBytes, targetChanPoint, nil + channel, err := c.kvStore.FetchChannelByID(id) + if err != nil { + return nil, err } - return c.channelScanner(nil, selector) + return c.attachOpenChannelStore(channel), nil } // ChanCount is used by the server in determining access control. @@ -789,373 +657,43 @@ type ChanCount = chanstate.ChanCount func (c *ChannelStateDB) FetchPermAndTempPeers( chainHash []byte) (map[string]ChanCount, error) { - peerChanInfo := make(map[string]ChanCount) - - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - openChanBucket := tx.ReadBucket(openChannelBucket) - if openChanBucket == nil { - return ErrNoChanDBExists - } - - // Hoist the outpoint-bucket lookup so the closed-channel check - // inside the nested chainBucket.ForEach below is a per-channel - // map probe rather than a tx-level bucket resolve. - opBucket := tx.ReadBucket(outpointBucket) - - openChanErr := openChanBucket.ForEach(func(nodePub, - v []byte) error { - - // If there is a value, this is not a bucket. - if v != nil { - return nil - } - - nodeChanBucket := openChanBucket.NestedReadBucket( - nodePub, - ) - if nodeChanBucket == nil { - return nil - } - - chainBucket := nodeChanBucket.NestedReadBucket( - chainHash, - ) - if chainBucket == nil { - return fmt.Errorf("no chain bucket exists") - } - - var isPermPeer bool - var pendingOpenCount uint64 - - internalErr := chainBucket.ForEach(func(chanPoint, - val []byte) error { - - // If there is a value, this is not a bucket. - if val != nil { - return nil - } - - // Skip already-closed channels: they are - // logically closed even though their - // per-channel state still resides under - // chainBucket. The closed peer's protected - // status is established below via the - // historical-channel scan. - isClosed, err := isOutpointClosed( - opBucket, chanPoint, - ) - if err != nil { - return err - } - if isClosed { - return nil - } - - chanBucket := chainBucket.NestedReadBucket( - chanPoint, - ) - if chanBucket == nil { - return nil - } - - var op wire.OutPoint - readErr := graphdb.ReadOutpoint( - bytes.NewReader(chanPoint), &op, - ) - if readErr != nil { - return readErr - } - - // We need to go through each channel and look - // at the IsPending status. - openChan, err := fetchOpenChannel( - chanBucket, &op, - ) - if err != nil { - return err - } - - if openChan.IsPending { - // Add to the pending-open count since - // this is a temp peer. - pendingOpenCount++ - return nil - } - - // Since IsPending is false, this is a perm - // peer. - isPermPeer = true - - return nil - }) - if internalErr != nil { - return internalErr - } - - peerCount := ChanCount{ - HasOpenOrClosedChan: isPermPeer, - PendingOpenCount: pendingOpenCount, - } - peerChanInfo[string(nodePub)] = peerCount - - return nil - }) - if openChanErr != nil { - return openChanErr - } - - // Now check the closed channel bucket. - historicalChanBucket := tx.ReadBucket(historicalChannelBucket) - if historicalChanBucket == nil { - return ErrNoHistoricalBucket - } - - historicalErr := historicalChanBucket.ForEach(func(chanPoint, - v []byte) error { - // Parse each nested bucket and the chanInfoKey to get - // the IsPending bool. This determines whether the - // peer is protected or not. - if v != nil { - // This is not a bucket. This is currently not - // possible. - return nil - } - - chanBucket := historicalChanBucket.NestedReadBucket( - chanPoint, - ) - if chanBucket == nil { - // This is not possible. - return fmt.Errorf("no historical channel " + - "bucket exists") - } - - var op wire.OutPoint - readErr := graphdb.ReadOutpoint( - bytes.NewReader(chanPoint), &op, - ) - if readErr != nil { - return readErr - } - - // This channel is closed, but the structure of the - // historical bucket is the same. This is by design, - // which means we can call fetchOpenChannel. - channel, fetchErr := fetchOpenChannel(chanBucket, &op) - if fetchErr != nil { - return fetchErr - } - - // Only include this peer in the protected class if - // the closing transaction confirmed. Note that - // CloseChannel can be called in the funding manager - // while IsPending is true which is why we need this - // special-casing to not count premature funding - // manager calls to CloseChannel. - if !channel.IsPending { - // Fetch the public key of the remote node. We - // need to use the string-ified serialized, - // compressed bytes as the key. - remotePub := channel.IdentityPub - remoteSer := remotePub.SerializeCompressed() - remoteKey := string(remoteSer) - - count, exists := peerChanInfo[remoteKey] - if exists { - count.HasOpenOrClosedChan = true - peerChanInfo[remoteKey] = count - } else { - peerCount := ChanCount{ - HasOpenOrClosedChan: true, - } - peerChanInfo[remoteKey] = peerCount - } - } - - return nil - }) - if historicalErr != nil { - return historicalErr - } - - return nil - }, func() { - clear(peerChanInfo) - }) - - return peerChanInfo, err -} - -// channelSelector describes a function that takes a chain-hash bucket from -// within the open-channel DB and returns the wanted channel point bytes, and -// channel point. It must return the ErrChannelNotFound error if the wanted -// channel is not in the given bucket. -type channelSelector func(chainBkt walletdb.ReadBucket) ([]byte, *wire.OutPoint, - error) - -// channelScanner will traverse the DB to each chain-hash bucket of each node -// pub-key bucket in the open-channel-bucket. The chanSelector will then be used -// to fetch the wanted channel outpoint from the chain bucket. -func (c *ChannelStateDB) channelScanner(tx kvdb.RTx, - chanSelect channelSelector) (*OpenChannel, error) { - - var ( - targetChan *OpenChannel - - // errChanFound is used to signal that the channel has been - // found so that iteration through the DB buckets can stop. - errChanFound = errors.New("channel found") - ) - - // chanScan will traverse the following bucket structure: - // * nodePub => chainHash => chanPoint - // - // At each level we go one further, ensuring that we're traversing the - // proper key (that's actually a bucket). By only reading the bucket - // structure and skipping fully decoding each channel, we save a good - // bit of CPU as we don't need to do things like decompress public - // keys. - chanScan := func(tx kvdb.RTx) error { - // Get the bucket dedicated to storing the metadata for open - // channels. - openChanBucket := tx.ReadBucket(openChannelBucket) - if openChanBucket == nil { - return ErrNoActiveChannels - } - - // Hoist the outpoint-bucket lookup so the closed-channel - // check inside the per-chain ForEach below pays one tx-level - // bucket resolve total instead of one per visited chanKey. - opBucket := tx.ReadBucket(outpointBucket) - - // Within the node channel bucket, are the set of node pubkeys - // we have channels with, we don't know the entire set, so we'll - // check them all. - return openChanBucket.ForEach(func(nodePub, v []byte) error { - // Ensure that this is a key the same size as a pubkey, - // and also that it leads directly to a bucket. - if len(nodePub) != 33 || v != nil { - return nil - } - - nodeChanBucket := openChanBucket.NestedReadBucket( - nodePub, - ) - if nodeChanBucket == nil { - return nil - } - - // The next layer down is all the chains that this node - // has channels on with us. - return nodeChanBucket.ForEach(func(chainHash, - v []byte) error { - - // If there's a value, it's not a bucket so - // ignore it. - if v != nil { - return nil - } - - chainBucket := nodeChanBucket.NestedReadBucket( - chainHash, - ) - if chainBucket == nil { - return fmt.Errorf("unable to read "+ - "bucket for chain=%x", - chainHash) - } - - // Finally, we reach the leaf bucket that stores - // all the chanPoints for this node. - targetChanBytes, chanPoint, err := chanSelect( - chainBucket, - ) - if errors.Is(err, ErrChannelNotFound) { - return nil - } else if err != nil { - return err - } - - // An already-closed channel is logically gone - // and must not be surfaced by lookup-style - // scans. - isClosed, err := isOutpointClosed( - opBucket, targetChanBytes, - ) - if err != nil { - return err - } - if isClosed { - return nil - } - - chanBucket := chainBucket.NestedReadBucket( - targetChanBytes, - ) - if chanBucket == nil { - return nil - } - - channel, err := fetchOpenChannel( - chanBucket, chanPoint, - ) - if err != nil { - return err - } - - targetChan = channel - targetChan.Db = c - - return errChanFound - }) - }) - } - - var err error - if tx == nil { - err = kvdb.View(c.backend, chanScan, func() {}) - } else { - err = chanScan(tx) - } - if err != nil && !errors.Is(err, errChanFound) { - return nil, err - } - - if targetChan != nil { - return targetChan, nil - } - - // If we can't find the channel, then we return with an error, as we - // have nothing to back up. - return nil, ErrChannelNotFound + return c.kvStore.FetchPermAndTempPeers(chainHash) } // FetchAllChannels attempts to retrieve all open channels currently stored // within the database, including pending open, fully open and channels waiting // for a closing transaction to confirm. func (c *ChannelStateDB) FetchAllChannels() ([]*OpenChannel, error) { - return fetchChannels(c) + channels, err := c.kvStore.FetchAllChannels() + if err != nil { + return nil, err + } + + return c.attachOpenChannelStores(channels), nil } // FetchAllOpenChannels will return all channels that have the funding // transaction confirmed, and is not waiting for a closing transaction to be // confirmed. func (c *ChannelStateDB) FetchAllOpenChannels() ([]*OpenChannel, error) { - return fetchChannels( - c, - pendingChannelFilter(false), - waitingCloseFilter(false), - ) + channels, err := c.kvStore.FetchAllOpenChannels() + if err != nil { + return nil, err + } + + return c.attachOpenChannelStores(channels), nil } // FetchPendingChannels will return channels that have completed the process of // generating and broadcasting funding transactions, but whose funding // transactions have yet to be confirmed on the blockchain. func (c *ChannelStateDB) FetchPendingChannels() ([]*OpenChannel, error) { - return fetchChannels(c, - pendingChannelFilter(true), - waitingCloseFilter(false), - ) + channels, err := c.kvStore.FetchPendingChannels() + if err != nil { + return nil, err + } + + return c.attachOpenChannelStores(channels), nil } // FetchWaitingCloseChannels will return all channels that have been opened, @@ -1163,138 +701,12 @@ func (c *ChannelStateDB) FetchPendingChannels() ([]*OpenChannel, error) { // // NOTE: This includes channels that are also pending to be opened. func (c *ChannelStateDB) FetchWaitingCloseChannels() ([]*OpenChannel, error) { - return fetchChannels( - c, waitingCloseFilter(true), - ) -} - -// fetchChannelsFilter applies a filter to channels retrieved in fetchchannels. -// A set of filters can be combined to filter across multiple dimensions. -type fetchChannelsFilter func(channel *OpenChannel) bool - -// pendingChannelFilter returns a filter based on whether channels are pending -// (ie, their funding transaction still needs to confirm). If pending is false, -// channels with confirmed funding transactions are returned. -func pendingChannelFilter(pending bool) fetchChannelsFilter { - return func(channel *OpenChannel) bool { - return channel.IsPending == pending - } -} - -// waitingCloseFilter returns a filter which filters channels based on whether -// they are awaiting the confirmation of their closing transaction. If waiting -// close is true, channels that have had their closing tx broadcast are -// included. If it is false, channels that are not awaiting confirmation of -// their close transaction are returned. -func waitingCloseFilter(waitingClose bool) fetchChannelsFilter { - return func(channel *OpenChannel) bool { - // If the channel is in any other state than Default, - // then it means it is waiting to be closed. - channelWaitingClose := - channel.ChanStatus() != ChanStatusDefault - - // Include the channel if it matches the value for - // waiting close that we are filtering on. - return channelWaitingClose == waitingClose - } -} - -// fetchChannels attempts to retrieve channels currently stored in the -// database. It takes a set of filters which are applied to each channel to -// obtain a set of channels with the desired set of properties. Only channels -// which have a true value returned for *all* of the filters will be returned. -// If no filters are provided, every channel in the open channels bucket will -// be returned. -func fetchChannels(c *ChannelStateDB, filters ...fetchChannelsFilter) ( - []*OpenChannel, error) { - - var channels []*OpenChannel - - err := kvdb.View(c.backend, func(tx kvdb.RTx) error { - // Get the bucket dedicated to storing the metadata for open - // channels. - openChanBucket := tx.ReadBucket(openChannelBucket) - if openChanBucket == nil { - return ErrNoActiveChannels - } - - // Next, fetch the bucket dedicated to storing metadata related - // to all nodes. All keys within this bucket are the serialized - // public keys of all our direct counterparties. - nodeMetaBucket := tx.ReadBucket(nodeInfoBucket) - if nodeMetaBucket == nil { - return fmt.Errorf("node bucket not created") - } - - // Finally for each node public key in the bucket, fetch all - // the channels related to this particular node. - return nodeMetaBucket.ForEach(func(k, v []byte) error { - nodeChanBucket := openChanBucket.NestedReadBucket(k) - if nodeChanBucket == nil { - return nil - } - - return nodeChanBucket.ForEach(func(chainHash, v []byte) error { - // If there's a value, it's not a bucket so - // ignore it. - if v != nil { - return nil - } - - // If we've found a valid chainhash bucket, - // then we'll retrieve that so we can extract - // all the channels. - chainBucket := nodeChanBucket.NestedReadBucket( - chainHash, - ) - if chainBucket == nil { - return fmt.Errorf("unable to read "+ - "bucket for chain=%x", chainHash[:]) - } - - nodeChans, err := c.fetchNodeChannels( - tx, chainBucket, - ) - if err != nil { - return fmt.Errorf("unable to read "+ - "channel for chain_hash=%x, "+ - "node_key=%x: %v", chainHash[:], k, err) - } - for _, channel := range nodeChans { - // includeChannel indicates whether the channel - // meets the criteria specified by our filters. - includeChannel := true - - // Run through each filter and check whether the - // channel should be included. - for _, f := range filters { - // If the channel fails the filter, set - // includeChannel to false and don't bother - // checking the remaining filters. - if !f(channel) { - includeChannel = false - break - } - } - - // If the channel passed every filter, include it in - // our set of channels. - if includeChannel { - channels = append(channels, channel) - } - } - return nil - }) - - }) - }, func() { - channels = nil - }) + channels, err := c.kvStore.FetchWaitingCloseChannels() if err != nil { return nil, err } - return channels, nil + return c.attachOpenChannelStores(channels), nil } // FetchClosedChannels attempts to fetch all closed channels from the database. diff --git a/channeldb/db_test.go b/channeldb/db_test.go index 1f87e3b668..7e075d3056 100644 --- a/channeldb/db_test.go +++ b/channeldb/db_test.go @@ -477,12 +477,12 @@ func TestAbandonChannel(t *testing.T) { require.NoError(t, err, "unable to abandon channel") } -// TestFetchChannels tests the filtering of open channels in fetchChannels. -// It tests the case where no filters are provided (which is equivalent to -// FetchAllOpenChannels) and every combination of pending and waiting close. +// TestFetchChannels tests the filtering of open channels exposed by the +// public fetch methods. func TestFetchChannels(t *testing.T) { // Create static channel IDs for each kind of channel retrieved by - // fetchChannels so that the expected channel IDs can be set in tests. + // the fetch methods so that the expected channel IDs can be set in + // tests. var ( // Pending is a channel that is pending open, and has not had // a close initiated. @@ -502,12 +502,12 @@ func TestFetchChannels(t *testing.T) { tests := []struct { name string - filters []fetchChannelsFilter + fetch func(*ChannelStateDB) ([]*OpenChannel, error) expectedChannels map[lnwire.ShortChannelID]bool }{ { - name: "get all channels", - filters: []fetchChannelsFilter{}, + name: "get all channels", + fetch: (*ChannelStateDB).FetchAllChannels, expectedChannels: map[lnwire.ShortChannelID]bool{ pendingChan: true, pendingWaitingChan: true, @@ -516,30 +516,22 @@ func TestFetchChannels(t *testing.T) { }, }, { - name: "pending channels", - filters: []fetchChannelsFilter{ - pendingChannelFilter(true), - }, + name: "pending channels", + fetch: (*ChannelStateDB).FetchPendingChannels, expectedChannels: map[lnwire.ShortChannelID]bool{ - pendingChan: true, - pendingWaitingChan: true, + pendingChan: true, }, }, { - name: "open channels", - filters: []fetchChannelsFilter{ - pendingChannelFilter(false), - }, + name: "open channels", + fetch: (*ChannelStateDB).FetchAllOpenChannels, expectedChannels: map[lnwire.ShortChannelID]bool{ - openChan: true, - openWaitingChan: true, + openChan: true, }, }, { - name: "waiting close channels", - filters: []fetchChannelsFilter{ - waitingCloseFilter(true), - }, + name: "waiting close channels", + fetch: (*ChannelStateDB).FetchWaitingCloseChannels, expectedChannels: map[lnwire.ShortChannelID]bool{ pendingWaitingChan: true, openWaitingChan: true, @@ -547,54 +539,30 @@ func TestFetchChannels(t *testing.T) { }, { name: "not waiting close channels", - filters: []fetchChannelsFilter{ - waitingCloseFilter(false), + fetch: func(cdb *ChannelStateDB) ( + []*OpenChannel, error) { + + pendingChans, err := cdb.FetchPendingChannels() + if err != nil { + return nil, err + } + + openChannels, err := cdb.FetchAllOpenChannels() + if err != nil { + return nil, err + } + + pendingChans = append( + pendingChans, openChannels..., + ) + + return pendingChans, nil }, expectedChannels: map[lnwire.ShortChannelID]bool{ pendingChan: true, openChan: true, }, }, - { - name: "pending waiting", - filters: []fetchChannelsFilter{ - pendingChannelFilter(true), - waitingCloseFilter(true), - }, - expectedChannels: map[lnwire.ShortChannelID]bool{ - pendingWaitingChan: true, - }, - }, - { - name: "pending, not waiting", - filters: []fetchChannelsFilter{ - pendingChannelFilter(true), - waitingCloseFilter(false), - }, - expectedChannels: map[lnwire.ShortChannelID]bool{ - pendingChan: true, - }, - }, - { - name: "open waiting", - filters: []fetchChannelsFilter{ - pendingChannelFilter(false), - waitingCloseFilter(true), - }, - expectedChannels: map[lnwire.ShortChannelID]bool{ - openWaitingChan: true, - }, - }, - { - name: "open, not waiting", - filters: []fetchChannelsFilter{ - pendingChannelFilter(false), - waitingCloseFilter(false), - }, - expectedChannels: map[lnwire.ShortChannelID]bool{ - openChan: true, - }, - }, } for _, test := range tests { @@ -652,7 +620,7 @@ func TestFetchChannels(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - channels, err := fetchChannels(cdb, test.filters...) + channels, err := test.fetch(cdb) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/chanstate/kv_open_channel.go b/chanstate/kv_open_channel.go index f8bb177eb0..1750faae01 100644 --- a/chanstate/kv_open_channel.go +++ b/chanstate/kv_open_channel.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/keychain" @@ -786,6 +787,726 @@ func SyncPendingOpenChannel(tx kvdb.RwTx, channel *OpenChannel, return FullSyncOpenChannel(tx, channel) } +// FetchOpenChannels starts a new database transaction and returns all stored +// currently active/open channels associated with the target nodeID. In the +// case that no active channels are known to have been created with this node, +// then a zero-length slice is returned. +func (s *KVStore) FetchOpenChannels(nodeID *btcec.PublicKey) ( + []*OpenChannel, error) { + + var channels []*OpenChannel + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + var err error + channels, err = FetchOpenChannelsTx(tx, nodeID) + + return err + }, func() { + channels = nil + }) + + return channels, err +} + +// FetchOpenChannelsTx uses an existing database transaction and returns all +// stored currently active/open channels associated with the target nodeID. In +// the case that no active channels are known to have been created with this +// node, then a zero-length slice is returned. +func FetchOpenChannelsTx(tx kvdb.RTx, + nodeID *btcec.PublicKey) ([]*OpenChannel, error) { + + // Get the bucket dedicated to storing the metadata for open channels. + openChanBucket := tx.ReadBucket(openChannelBucket) + if openChanBucket == nil { + return nil, nil + } + + // Within this top level bucket, fetch the bucket dedicated to storing + // open channel data specific to the remote node. + pub := nodeID.SerializeCompressed() + nodeChanBucket := openChanBucket.NestedReadBucket(pub) + if nodeChanBucket == nil { + return nil, nil + } + + // Next, we'll need to go down an additional layer in order to retrieve + // the channels for each chain the node knows of. + var channels []*OpenChannel + err := nodeChanBucket.ForEach(func(chainHash, v []byte) error { + // If there's a value, it's not a bucket so ignore it. + if v != nil { + return nil + } + + // If we've found a valid chainhash bucket, then we'll retrieve + // that so we can extract all the channels. + chainBucket := nodeChanBucket.NestedReadBucket(chainHash) + if chainBucket == nil { + return fmt.Errorf("unable to read bucket for chain=%x", + chainHash) + } + + // Finally, we both of the necessary buckets retrieved, fetch + // all the active channels related to this node. + nodeChannels, err := FetchNodeChannels(tx, chainBucket) + if err != nil { + return fmt.Errorf("unable to read channel for "+ + "chain_hash=%x, node_key=%x: %v", + chainHash, pub, err) + } + + channels = append(channels, nodeChannels...) + + return nil + }) + + return channels, err +} + +// FetchNodeChannels retrieves all active channels from the target chainBucket +// which is under a node's dedicated channel bucket. This function is typically +// used to fetch all the active channels related to a particular node. Channels +// already flipped to outpointClosed in the outpoint index are skipped silently +// — readers see only channels that are still considered open. +func FetchNodeChannels(tx kvdb.RTx, + chainBucket kvdb.RBucket) ([]*OpenChannel, error) { + + var channels []*OpenChannel + + // Hoist the outpoint-bucket lookup so the closed-channel check inside + // the loop is a per-iteration map probe rather than a tx-level bucket + // resolve. + opBucket := tx.ReadBucket(outpointBucket) + + // A node may have channels on several chains, so for each known chain, + // we'll extract all the channels. + err := chainBucket.ForEach(func(chanPoint, v []byte) error { + // If there's a value, it's not a bucket so ignore it. + if v != nil { + return nil + } + + // Skip already-closed channels. The chanBucket still exists + // on disk on tombstone-enabled backends; the outpoint flip is + // the sole signal that the channel should be treated as + // closed. + isClosed, err := IsOutpointClosed(opBucket, chanPoint) + if err != nil { + return err + } + if isClosed { + return nil + } + + // Once we've found a valid channel bucket, we'll extract it + // from the node's chain bucket. + chanBucket := chainBucket.NestedReadBucket(chanPoint) + + var outPoint wire.OutPoint + err = graphdb.ReadOutpoint( + bytes.NewReader(chanPoint), &outPoint, + ) + if err != nil { + return err + } + oChannel, err := FetchOpenChannel(chanBucket, &outPoint) + if err != nil { + return fmt.Errorf("unable to read channel data for "+ + "chan_point=%v: %w", outPoint, err) + } + + channels = append(channels, oChannel) + + return nil + }) + if err != nil { + return nil, err + } + + return channels, nil +} + +// FetchChannel attempts to locate a channel specified by the passed channel +// point. If the channel cannot be found, then an error will be returned. +func (s *KVStore) FetchChannel(chanPoint wire.OutPoint) (*OpenChannel, + error) { + + var targetChanPoint bytes.Buffer + err := graphdb.WriteOutpoint(&targetChanPoint, &chanPoint) + if err != nil { + return nil, err + } + + targetChanPointBytes := targetChanPoint.Bytes() + selector := func(chainBkt walletdb.ReadBucket) ([]byte, *wire.OutPoint, + error) { + + return targetChanPointBytes, &chanPoint, nil + } + + return s.channelScanner(nil, selector) +} + +// FetchChannelByID attempts to locate a channel specified by the passed channel +// ID. If the channel cannot be found, then an error will be returned. +func (s *KVStore) FetchChannelByID(id lnwire.ChannelID) (*OpenChannel, + error) { + + selector := func(chainBkt walletdb.ReadBucket) ([]byte, *wire.OutPoint, + error) { + + var ( + targetChanPointBytes []byte + targetChanPoint *wire.OutPoint + + // errChanFound is used to signal that the channel has + // been found so that iteration through the DB buckets + // can stop. + errChanFound = errors.New("channel found") + ) + err := chainBkt.ForEach(func(k, _ []byte) error { + var outPoint wire.OutPoint + err := graphdb.ReadOutpoint( + bytes.NewReader(k), &outPoint, + ) + if err != nil { + return err + } + + chanID := lnwire.NewChanIDFromOutPoint(outPoint) + if chanID != id { + return nil + } + + targetChanPoint = &outPoint + targetChanPointBytes = k + + return errChanFound + }) + if err != nil && !errors.Is(err, errChanFound) { + return nil, nil, err + } + if targetChanPoint == nil { + return nil, nil, ErrChannelNotFound + } + + return targetChanPointBytes, targetChanPoint, nil + } + + return s.channelScanner(nil, selector) +} + +// FetchPermAndTempPeers returns a map where the key is the remote node's +// public key and the value is a struct that has a tally of the pending-open +// channels and whether the peer has an open or closed channel with us. +func (s *KVStore) FetchPermAndTempPeers( + chainHash []byte) (map[string]ChanCount, error) { + + peerChanInfo := make(map[string]ChanCount) + + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + openChanBucket := tx.ReadBucket(openChannelBucket) + if openChanBucket == nil { + return ErrNoChanDBExists + } + + // Hoist the outpoint-bucket lookup so the closed-channel check + // inside the nested chainBucket.ForEach below is a per-channel + // map probe rather than a tx-level bucket resolve. + opBucket := tx.ReadBucket(outpointBucket) + + openChanErr := openChanBucket.ForEach(func(nodePub, + v []byte) error { + + // If there is a value, this is not a bucket. + if v != nil { + return nil + } + + nodeChanBucket := openChanBucket.NestedReadBucket( + nodePub, + ) + if nodeChanBucket == nil { + return nil + } + + chainBucket := nodeChanBucket.NestedReadBucket( + chainHash, + ) + if chainBucket == nil { + return fmt.Errorf("no chain bucket exists") + } + + var isPermPeer bool + var pendingOpenCount uint64 + + internalErr := chainBucket.ForEach(func(chanPoint, + val []byte) error { + + // If there is a value, this is not a bucket. + if val != nil { + return nil + } + + // Skip already-closed channels: they are + // logically closed even though their + // per-channel state still resides under + // chainBucket. The closed peer's protected + // status is established below via the + // historical-channel scan. + isClosed, err := IsOutpointClosed( + opBucket, chanPoint, + ) + if err != nil { + return err + } + if isClosed { + return nil + } + + chanBucket := chainBucket.NestedReadBucket( + chanPoint, + ) + if chanBucket == nil { + return nil + } + + var op wire.OutPoint + readErr := graphdb.ReadOutpoint( + bytes.NewReader(chanPoint), &op, + ) + if readErr != nil { + return readErr + } + + // We need to go through each channel and look + // at the IsPending status. + openChan, err := FetchOpenChannel( + chanBucket, &op, + ) + if err != nil { + return err + } + + if openChan.IsPending { + // Add to the pending-open count since + // this is a temp peer. + pendingOpenCount++ + return nil + } + + // Since IsPending is false, this is a perm + // peer. + isPermPeer = true + + return nil + }) + if internalErr != nil { + return internalErr + } + + peerCount := ChanCount{ + HasOpenOrClosedChan: isPermPeer, + PendingOpenCount: pendingOpenCount, + } + peerChanInfo[string(nodePub)] = peerCount + + return nil + }) + if openChanErr != nil { + return openChanErr + } + + // Now check the closed channel bucket. + historicalChanBucket := tx.ReadBucket(historicalChannelBucket) + if historicalChanBucket == nil { + return ErrNoHistoricalBucket + } + + historicalErr := historicalChanBucket.ForEach(func(chanPoint, + v []byte) error { + + // Parse each nested bucket and the chanInfoKey to get + // the IsPending bool. This determines whether the + // peer is protected or not. + if v != nil { + // This is not a bucket. This is currently not + // possible. + return nil + } + + chanBucket := historicalChanBucket.NestedReadBucket( + chanPoint, + ) + if chanBucket == nil { + // This is not possible. + return fmt.Errorf("no historical channel " + + "bucket exists") + } + + var op wire.OutPoint + readErr := graphdb.ReadOutpoint( + bytes.NewReader(chanPoint), &op, + ) + if readErr != nil { + return readErr + } + + // This channel is closed, but the structure of the + // historical bucket is the same. This is by design, + // which means we can call FetchOpenChannel. + channel, fetchErr := FetchOpenChannel(chanBucket, &op) + if fetchErr != nil { + return fetchErr + } + + // Only include this peer in the protected class if + // the closing transaction confirmed. Note that + // CloseChannel can be called in the funding manager + // while IsPending is true which is why we need this + // special-casing to not count premature funding + // manager calls to CloseChannel. + if !channel.IsPending { + // Fetch the public key of the remote node. We + // need to use the string-ified serialized, + // compressed bytes as the key. + remotePub := channel.IdentityPub + remoteSer := remotePub.SerializeCompressed() + remoteKey := string(remoteSer) + + count, exists := peerChanInfo[remoteKey] + if exists { + count.HasOpenOrClosedChan = true + peerChanInfo[remoteKey] = count + } else { + peerCount := ChanCount{ + HasOpenOrClosedChan: true, + } + peerChanInfo[remoteKey] = peerCount + } + } + + return nil + }) + if historicalErr != nil { + return historicalErr + } + + return nil + }, func() { + clear(peerChanInfo) + }) + + return peerChanInfo, err +} + +// channelSelector describes a function that takes a chain-hash bucket from +// within the open-channel DB and returns the wanted channel point bytes, and +// channel point. It must return the ErrChannelNotFound error if the wanted +// channel is not in the given bucket. +type channelSelector func(chainBkt walletdb.ReadBucket) ([]byte, + *wire.OutPoint, error) + +// channelScanner will traverse the DB to each chain-hash bucket of each node +// pub-key bucket in the open-channel-bucket. The chanSelector will then be used +// to fetch the wanted channel outpoint from the chain bucket. +func (s *KVStore) channelScanner(tx kvdb.RTx, + chanSelect channelSelector) (*OpenChannel, error) { + + var ( + targetChan *OpenChannel + + // errChanFound is used to signal that the channel has been + // found so that iteration through the DB buckets can stop. + errChanFound = errors.New("channel found") + ) + + // chanScan will traverse the following bucket structure: + // * nodePub => chainHash => chanPoint + // + // At each level we go one further, ensuring that we're traversing the + // proper key (that's actually a bucket). By only reading the bucket + // structure and skipping fully decoding each channel, we save a good + // bit of CPU as we don't need to do things like decompress public + // keys. + chanScan := func(tx kvdb.RTx) error { + // Get the bucket dedicated to storing the metadata for open + // channels. + openChanBucket := tx.ReadBucket(openChannelBucket) + if openChanBucket == nil { + return ErrNoActiveChannels + } + + // Hoist the outpoint-bucket lookup so the closed-channel + // check inside the per-chain ForEach below pays one tx-level + // bucket resolve total instead of one per visited chanKey. + opBucket := tx.ReadBucket(outpointBucket) + + // Within the node channel bucket, are the set of node pubkeys + // we have channels with, we don't know the entire set, so we'll + // check them all. + return openChanBucket.ForEach(func(nodePub, v []byte) error { + // Ensure that this is a key the same size as a pubkey, + // and also that it leads directly to a bucket. + if len(nodePub) != 33 || v != nil { + return nil + } + + nodeChanBucket := openChanBucket.NestedReadBucket( + nodePub, + ) + if nodeChanBucket == nil { + return nil + } + + // The next layer down is all the chains that this node + // has channels on with us. + return nodeChanBucket.ForEach(func(chainHash, + v []byte) error { + + // If there's a value, it's not a bucket so + // ignore it. + if v != nil { + return nil + } + + chainBucket := nodeChanBucket.NestedReadBucket( + chainHash, + ) + if chainBucket == nil { + return fmt.Errorf("unable to read "+ + "bucket for chain=%x", + chainHash) + } + + // Finally, we reach the leaf bucket that stores + // all the chanPoints for this node. + targetChanBytes, chanPoint, err := chanSelect( + chainBucket, + ) + if errors.Is(err, ErrChannelNotFound) { + return nil + } else if err != nil { + return err + } + + // An already-closed channel is logically gone + // and must not be surfaced by lookup-style + // scans. + isClosed, err := IsOutpointClosed( + opBucket, targetChanBytes, + ) + if err != nil { + return err + } + if isClosed { + return nil + } + + chanBucket := chainBucket.NestedReadBucket( + targetChanBytes, + ) + if chanBucket == nil { + return nil + } + + channel, err := FetchOpenChannel( + chanBucket, chanPoint, + ) + if err != nil { + return err + } + + targetChan = channel + + return errChanFound + }) + }) + } + + var err error + if tx == nil { + err = kvdb.View(s.backend, chanScan, func() {}) + } else { + err = chanScan(tx) + } + if err != nil && !errors.Is(err, errChanFound) { + return nil, err + } + + if targetChan != nil { + return targetChan, nil + } + + // If we can't find the channel, then we return with an error, as we + // have nothing to back up. + return nil, ErrChannelNotFound +} + +// FetchAllChannels attempts to retrieve all open channels currently stored +// within the database, including pending open, fully open and channels waiting +// for a closing transaction to confirm. +func (s *KVStore) FetchAllChannels() ([]*OpenChannel, error) { + return s.fetchChannels() +} + +// FetchAllOpenChannels will return all channels that have the funding +// transaction confirmed, and is not waiting for a closing transaction to be +// confirmed. +func (s *KVStore) FetchAllOpenChannels() ([]*OpenChannel, error) { + return s.fetchChannels( + pendingChannelFilter(false), + waitingCloseFilter(false), + ) +} + +// FetchPendingChannels will return channels that have completed the process of +// generating and broadcasting funding transactions, but whose funding +// transactions have yet to be confirmed on the blockchain. +func (s *KVStore) FetchPendingChannels() ([]*OpenChannel, error) { + return s.fetchChannels( + pendingChannelFilter(true), + waitingCloseFilter(false), + ) +} + +// FetchWaitingCloseChannels will return all channels that have been opened, +// but are now waiting for a closing transaction to be confirmed. +// +// NOTE: This includes channels that are also pending to be opened. +func (s *KVStore) FetchWaitingCloseChannels() ([]*OpenChannel, error) { + return s.fetchChannels(waitingCloseFilter(true)) +} + +// fetchChannelsFilter applies a filter to channels retrieved in fetchchannels. +// A set of filters can be combined to filter across multiple dimensions. +type fetchChannelsFilter func(channel *OpenChannel) bool + +// pendingChannelFilter returns a filter based on whether channels are pending +// (ie, their funding transaction still needs to confirm). If pending is false, +// channels with confirmed funding transactions are returned. +func pendingChannelFilter(pending bool) fetchChannelsFilter { + return func(channel *OpenChannel) bool { + return channel.IsPending == pending + } +} + +// waitingCloseFilter returns a filter which filters channels based on whether +// they are awaiting the confirmation of their closing transaction. If waiting +// close is true, channels that have had their closing tx broadcast are +// included. If it is false, channels that are not awaiting confirmation of +// their close transaction are returned. +func waitingCloseFilter(waitingClose bool) fetchChannelsFilter { + return func(channel *OpenChannel) bool { + // If the channel is in any other state than Default, + // then it means it is waiting to be closed. + channelWaitingClose := + channel.ChanStatus() != ChanStatusDefault + + // Include the channel if it matches the value for + // waiting close that we are filtering on. + return channelWaitingClose == waitingClose + } +} + +// fetchChannels attempts to retrieve channels currently stored in the +// database. It takes a set of filters which are applied to each channel to +// obtain a set of channels with the desired set of properties. Only channels +// which have a true value returned for *all* of the filters will be returned. +// If no filters are provided, every channel in the open channels bucket will +// be returned. +func (s *KVStore) fetchChannels(filters ...fetchChannelsFilter) ( + []*OpenChannel, error) { + + var channels []*OpenChannel + addChannel := func(channel *OpenChannel) { + channels = append(channels, channel) + } + + err := kvdb.View(s.backend, func(tx kvdb.RTx) error { + // Get the bucket dedicated to storing the metadata for open + // channels. + openChanBucket := tx.ReadBucket(openChannelBucket) + if openChanBucket == nil { + return ErrNoActiveChannels + } + + // Finally for each node public key in the open channel + // bucket, fetch all the channels related to this particular + // node. + return openChanBucket.ForEach(func(k, v []byte) error { + // Ensure that this is a key the same size as a pubkey, + // and also that it leads directly to a bucket. + if len(k) != 33 || v != nil { + return nil + } + + nodeChanBucket := openChanBucket.NestedReadBucket(k) + if nodeChanBucket == nil { + return nil + } + + return nodeChanBucket.ForEach(func(chainHash, + v []byte) error { + + // If there's a value, it's not a bucket so + // ignore it. + if v != nil { + return nil + } + + // If we've found a valid chainhash bucket, + // then we'll retrieve that so we can extract + // all the channels. + chainBucket := nodeChanBucket.NestedReadBucket( + chainHash, + ) + if chainBucket == nil { + return fmt.Errorf("unable to read "+ + "chain bucket %x", chainHash) + } + + nodeChans, err := FetchNodeChannels( + tx, chainBucket, + ) + if err != nil { + return fmt.Errorf("unable to read "+ + "channel chain=%x node=%x: %v", + chainHash, k, err) + } + for _, channel := range nodeChans { + // includeChannel indicates whether + // the channel meets our filters. + includeChannel := true + + // Check each filter. + for _, f := range filters { + // Stop once one filter fails. + if !f(channel) { + includeChannel = false + break + } + } + + // If the channel passed every filter, + // include it in our set of channels. + if includeChannel { + addChannel(channel) + } + } + + return nil + }) + }) + }, func() { + channels = nil + }) + if err != nil { + return nil, err + } + + return channels, nil +} + // FetchHistoricalChanBucket returns a the channel bucket for a given outpoint // from the historical channel bucket. If the bucket does not exist, // ErrNoHistoricalBucket is returned. From 9178c927cbfc57eaa0ee5b59990f4c296c3c6f22 Mon Sep 17 00:00:00 2001 From: ziggie Date: Mon, 18 May 2026 14:04:27 -0300 Subject: [PATCH 61/61] chanstate: move abandon channel kv store Move the KV-backed AbandonChannel implementation onto KVStore. ChannelStateDB keeps a compatibility wrapper while callers still import channeldb for the channel-state store. Keep the link-node-coupled close and repair methods in channeldb so the follow-up LinkNode store can define that boundary explicitly. --- channeldb/channel.go | 12 -------- channeldb/channel_test.go | 5 ++-- channeldb/db.go | 43 +---------------------------- chanstate/kv_close_summary.go | 52 +++++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 57 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 57e065a070..d3853a37b5 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -113,18 +113,6 @@ const ( outpointClosed = cstate.OutpointClosed ) -// 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) { - return cstate.IsOutpointClosed(opBucket, chanKey) -} - // ChannelType is an enum-like type that describes one of several possible // channel types. type ChannelType = cstate.ChannelType diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 215855e644..d26d166a55 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -1438,9 +1438,8 @@ func TestRefresh(t *testing.T) { t.Fatalf("channel pending state wasn't updated: want false got true") } - require.Equal( - t, chanOpenLoc, NewChannelPackager(state.ShortChanID()).Source(), - ) + source := NewChannelPackager(state.ShortChanID()).Source() + require.Equal(t, chanOpenLoc, source) } // TestCloseInitiator tests the setting of close initiator statuses for diff --git a/channeldb/db.go b/channeldb/db.go index 1aa8e7fa8a..09d185c6ec 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -1059,48 +1059,7 @@ func (d *DB) AddrsForNode(_ context.Context, nodePub *btcec.PublicKey) (bool, func (c *ChannelStateDB) AbandonChannel(chanPoint *wire.OutPoint, bestHeight uint32) error { - // With the chanPoint constructed, we'll attempt to find the target - // channel in the database. If we can't find the channel, then we'll - // return the error back to the caller. - dbChan, err := c.FetchChannel(*chanPoint) - switch { - // If the channel wasn't found, then it's possible that it was already - // abandoned from the database. - case err == ErrChannelNotFound: - _, closedErr := c.FetchClosedChannel(chanPoint) - if closedErr != nil { - return closedErr - } - - // If the channel was already closed, then we don't return an - // error as we'd like this step to be repeatable. - return nil - case err != nil: - return err - } - - // Now that we've found the channel, we'll populate a close summary for - // the channel, so we can store as much information for this abounded - // channel as possible. We also ensure that we set Pending to false, to - // indicate that this channel has been "fully" closed. - summary := &ChannelCloseSummary{ - CloseType: Abandoned, - ChanPoint: *chanPoint, - ChainHash: dbChan.ChainHash, - CloseHeight: bestHeight, - RemotePub: dbChan.IdentityPub, - Capacity: dbChan.Capacity, - SettledBalance: dbChan.LocalCommitment.LocalBalance.ToSatoshis(), - ShortChanID: dbChan.ShortChanID(), - RemoteCurrentRevocation: dbChan.RemoteCurrentRevocation, - RemoteNextRevocation: dbChan.RemoteNextRevocation, - LocalChanConfig: dbChan.LocalChanCfg, - } - - // Finally, we'll close the channel in the DB, and return back to the - // caller. We set ourselves as the close initiator because we abandoned - // the channel. - return dbChan.CloseChannel(summary, ChanStatusLocalCloseInitiator) + return c.kvStore.AbandonChannel(chanPoint, bestHeight) } // SaveChannelOpeningState saves the serialized channel state for the provided diff --git a/chanstate/kv_close_summary.go b/chanstate/kv_close_summary.go index a9a4a5f03e..b2653c478a 100644 --- a/chanstate/kv_close_summary.go +++ b/chanstate/kv_close_summary.go @@ -380,6 +380,58 @@ func (s *KVStore) FetchClosedChannelForID(cid lnwire.ChannelID) ( return chanSummary, nil } +// AbandonChannel attempts to remove the target channel from the open channel +// database. If the channel was already removed (has a closed channel entry), +// then we'll return a nil error. Otherwise, we'll insert a new close summary +// into the database. +func (s *KVStore) AbandonChannel(chanPoint *wire.OutPoint, + bestHeight uint32) error { + + // With the chanPoint constructed, we'll attempt to find the target + // channel in the database. If we can't find the channel, then we'll + // return the error back to the caller. + dbChan, err := s.FetchChannel(*chanPoint) + switch { + // If the channel wasn't found, then it's possible that it was already + // abandoned from the database. + case errors.Is(err, ErrChannelNotFound): + _, closedErr := s.FetchClosedChannel(chanPoint) + if closedErr != nil { + return closedErr + } + + // If the channel was already closed, then we don't return an + // error as we'd like this step to be repeatable. + return nil + case err != nil: + return err + } + + // Now that we've found the channel, we'll populate a close summary for + // the channel, so we can store as much information for this abounded + // channel as possible. We also ensure that we set Pending to false, to + // indicate that this channel has been "fully" closed. + settledBalance := dbChan.LocalCommitment.LocalBalance.ToSatoshis() + summary := &ChannelCloseSummary{ + CloseType: Abandoned, + ChanPoint: *chanPoint, + ChainHash: dbChan.ChainHash, + CloseHeight: bestHeight, + RemotePub: dbChan.IdentityPub, + Capacity: dbChan.Capacity, + SettledBalance: settledBalance, + ShortChanID: dbChan.ShortChanID(), + RemoteCurrentRevocation: dbChan.RemoteCurrentRevocation, + RemoteNextRevocation: dbChan.RemoteNextRevocation, + LocalChanConfig: dbChan.LocalChanCfg, + } + + // Finally, we'll close the channel in the DB, and return back to the + // caller. We set ourselves as the close initiator because we abandoned + // the channel. + return s.CloseChannel(dbChan, summary, ChanStatusLocalCloseInitiator) +} + // SerializeChannelCloseSummary serializes a channel close summary. func SerializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error {