From db6c4038ab002c4b35f930cb128067de84681e39 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 16:45:00 -0300 Subject: [PATCH 01/33] chanstate: make store channel types generic Move the small value types referenced by chanstate.Store out of channeldb. This includes ChannelConfig, ChannelStatus, ChannelCloseSummary, ChannelShell, ChanCount, and FinalHtlcInfo. Leave aliases in channeldb so existing callers keep compiling while the backend still lives there. Parameterize the Store facets over the channel type and instantiate current callers with *channeldb.OpenChannel. This removes the chanstate -> channeldb import edge without moving OpenChannel yet, keeping the first step reviewable and backend-neutral. --- channeldb/channel.go | 344 ++++------------------------- channeldb/chanstate_assertions.go | 7 + channeldb/db.go | 19 +- channelnotifier/channelnotifier.go | 4 +- chanrestore.go | 2 +- chanstate/channel.go | 18 ++ chanstate/channel_status.go | 110 +++++++++ chanstate/close_summary.go | 126 +++++++++++ chanstate/config.go | 108 +++++++++ chanstate/interface.go | 66 ++---- chanstate/open_channel_types.go | 16 ++ contractcourt/breach_arbitrator.go | 3 +- funding/manager.go | 2 +- lnrpc/invoicesrpc/addinvoice.go | 2 +- lnrpc/invoicesrpc/config_active.go | 3 +- lnrpc/walletrpc/config_active.go | 3 +- peer/brontide.go | 2 +- server.go | 2 +- subrpcserver_config.go | 3 +- 19 files changed, 475 insertions(+), 365 deletions(-) create mode 100644 channeldb/chanstate_assertions.go create mode 100644 chanstate/channel.go create mode 100644 chanstate/channel_status.go create mode 100644 chanstate/close_summary.go create mode 100644 chanstate/config.go create mode 100644 chanstate/open_channel_types.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 127e0ac9c4..7ddbcdd4f0 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -9,8 +9,6 @@ import ( "fmt" "io" "net" - "strconv" - "strings" "sync" "github.com/btcsuite/btcd/btcec/v2" @@ -19,6 +17,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/walletdb" + 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" @@ -556,105 +555,16 @@ func (c ChannelType) IsTaprootFinal() bool { } // ChannelStateBounds are the parameters from OpenChannel and AcceptChannel -// that are responsible for providing bounds on the state space of the abstract -// channel state. These values must be remembered for normal channel operation -// but they do not impact how we compute the commitment transactions themselves. -type ChannelStateBounds struct { - // ChanReserve is an absolute reservation on the channel for the - // owner of this set of constraints. This means that the current - // settled balance for this node CANNOT dip below the reservation - // amount. This acts as a defense against costless attacks when - // either side no longer has any skin in the game. - ChanReserve btcutil.Amount - - // MaxPendingAmount is the maximum pending HTLC value that the - // owner of these constraints can offer the remote node at a - // particular time. - MaxPendingAmount lnwire.MilliSatoshi - - // MinHTLC is the minimum HTLC value that the owner of these - // constraints can offer the remote node. If any HTLCs below this - // amount are offered, then the HTLC will be rejected. This, in - // tandem with the dust limit allows a node to regulate the - // smallest HTLC that it deems economically relevant. - MinHTLC lnwire.MilliSatoshi - - // MaxAcceptedHtlcs is the maximum number of HTLCs that the owner of - // this set of constraints can offer the remote node. This allows each - // node to limit their over all exposure to HTLCs that may need to be - // acted upon in the case of a unilateral channel closure or a contract - // breach. - MaxAcceptedHtlcs uint16 -} - -// CommitmentParams are the parameters from OpenChannel and -// AcceptChannel that are required to render an abstract channel state to a -// concrete commitment transaction. These values are necessary to (re)compute -// the commitment transaction. We treat these differently than the state space -// bounds because their history needs to be stored in order to properly handle -// chain resolution. -type CommitmentParams struct { - // DustLimit is the threshold (in satoshis) below which any outputs - // should be trimmed. When an output is trimmed, it isn't materialized - // as an actual output, but is instead burned to miner's fees. - DustLimit btcutil.Amount - - // CsvDelay is the relative time lock delay expressed in blocks. Any - // settled outputs that pay to the owner of this channel configuration - // MUST ensure that the delay branch uses this value as the relative - // time lock. Similarly, any HTLC's offered by this node should use - // this value as well. - CsvDelay uint16 -} - -// ChannelConfig is a struct that houses the various configuration opens for -// channels. Each side maintains an instance of this configuration file as it -// governs: how the funding and commitment transaction to be created, the -// nature of HTLC's allotted, the keys to be used for delivery, and relative -// time lock parameters. -type ChannelConfig struct { - // ChannelStateBounds is the set of constraints that must be - // upheld for the duration of the channel for the owner of this channel - // configuration. Constraints govern a number of flow control related - // parameters, also including the smallest HTLC that will be accepted - // by a participant. - ChannelStateBounds - - // CommitmentParams is an embedding of the parameters - // required to render an abstract channel state into a concrete - // commitment transaction. - CommitmentParams - - // MultiSigKey is the key to be used within the 2-of-2 output script - // for the owner of this channel config. - MultiSigKey keychain.KeyDescriptor - - // RevocationBasePoint is the base public key to be used when deriving - // revocation keys for the remote node's commitment transaction. This - // will be combined along with a per commitment secret to derive a - // unique revocation key for each state. - RevocationBasePoint keychain.KeyDescriptor - - // PaymentBasePoint is the base public key to be used when deriving - // the key used within the non-delayed pay-to-self output on the - // commitment transaction for a node. This will be combined with a - // tweak derived from the per-commitment point to ensure unique keys - // for each commitment transaction. - PaymentBasePoint keychain.KeyDescriptor - - // DelayBasePoint is the base public key to be used when deriving the - // key used within the delayed pay-to-self output on the commitment - // transaction for a node. This will be combined with a tweak derived - // from the per-commitment point to ensure unique keys for each - // commitment transaction. - DelayBasePoint keychain.KeyDescriptor - - // HtlcBasePoint is the base public key to be used when deriving the - // local HTLC key. The derived key (combined with the tweak derived - // from the per-commitment point) is used within the "to self" clause - // within any HTLC output scripts. - HtlcBasePoint keychain.KeyDescriptor -} +// that bound the abstract channel state. +type ChannelStateBounds = cstate.ChannelStateBounds + +// CommitmentParams are the parameters from OpenChannel and AcceptChannel that +// are required to render an abstract channel state to a concrete commitment +// transaction. +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. @@ -834,108 +744,41 @@ func (c *ChannelCommitment) copy() ChannelCommitment { // 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 uint64 +type ChannelStatus = cstate.ChannelStatus var ( // ChanStatusDefault is the normal state of an open channel. - ChanStatusDefault ChannelStatus + ChanStatusDefault = cstate.ChanStatusDefault // ChanStatusBorked indicates that the channel has entered an - // irreconcilable state, triggered by a state desynchronization or - // channel breach. Channels in this state should never be added to the - // htlc switch. - ChanStatusBorked ChannelStatus = 1 + // irreconcilable state. + ChanStatusBorked = cstate.ChanStatusBorked // ChanStatusCommitBroadcasted indicates that a commitment for this // channel has been broadcasted. - ChanStatusCommitBroadcasted ChannelStatus = 1 << 1 + ChanStatusCommitBroadcasted = cstate.ChanStatusCommitBroadcasted // ChanStatusLocalDataLoss indicates that we have lost channel state - // for this channel, and broadcasting our latest commitment might be - // considered a breach. - // - // TODO(halseh): actually enforce that we are not force closing such a + // for this channel. + ChanStatusLocalDataLoss = cstate.ChanStatusLocalDataLoss + + // ChanStatusRestored signals that the channel has been restored and + // doesn't have all fields a typical channel will have. + ChanStatusRestored = cstate.ChanStatusRestored + + // ChanStatusCoopBroadcasted indicates that a cooperative close for this + // channel has been broadcasted. + ChanStatusCoopBroadcasted = cstate.ChanStatusCoopBroadcasted + + // ChanStatusLocalCloseInitiator indicates that we initiated closing the // channel. - ChanStatusLocalDataLoss ChannelStatus = 1 << 2 - - // ChanStatusRestored is a status flag that signals that the channel - // has been restored, and doesn't have all the fields a typical channel - // will have. - ChanStatusRestored ChannelStatus = 1 << 3 - - // ChanStatusCoopBroadcasted indicates that a cooperative close for - // this channel has been broadcasted. Older cooperatively closed - // channels will only have this status set. Newer ones will also have - // close initiator information stored using the local/remote initiator - // status. This status is set in conjunction with the initiator status - // so that we do not need to check multiple channel statues for - // cooperative closes. - ChanStatusCoopBroadcasted ChannelStatus = 1 << 4 - - // ChanStatusLocalCloseInitiator indicates that we initiated closing - // the channel. - ChanStatusLocalCloseInitiator ChannelStatus = 1 << 5 + ChanStatusLocalCloseInitiator = cstate.ChanStatusLocalCloseInitiator // ChanStatusRemoteCloseInitiator indicates that the remote node // initiated closing the channel. - ChanStatusRemoteCloseInitiator ChannelStatus = 1 << 6 + ChanStatusRemoteCloseInitiator = cstate.ChanStatusRemoteCloseInitiator ) -// chanStatusStrings maps a ChannelStatus to a human friendly string that -// describes that status. -var chanStatusStrings = map[ChannelStatus]string{ - ChanStatusDefault: "ChanStatusDefault", - ChanStatusBorked: "ChanStatusBorked", - ChanStatusCommitBroadcasted: "ChanStatusCommitBroadcasted", - ChanStatusLocalDataLoss: "ChanStatusLocalDataLoss", - ChanStatusRestored: "ChanStatusRestored", - ChanStatusCoopBroadcasted: "ChanStatusCoopBroadcasted", - ChanStatusLocalCloseInitiator: "ChanStatusLocalCloseInitiator", - ChanStatusRemoteCloseInitiator: "ChanStatusRemoteCloseInitiator", -} - -// orderedChanStatusFlags is an in-order list of all that channel status flags. -var orderedChanStatusFlags = []ChannelStatus{ - ChanStatusBorked, - ChanStatusCommitBroadcasted, - ChanStatusLocalDataLoss, - ChanStatusRestored, - ChanStatusCoopBroadcasted, - ChanStatusLocalCloseInitiator, - ChanStatusRemoteCloseInitiator, -} - -// String returns a human-readable representation of the ChannelStatus. -func (c ChannelStatus) String() string { - // If no flags are set, then this is the default case. - if c == ChanStatusDefault { - return chanStatusStrings[ChanStatusDefault] - } - - // Add individual bit flags. - statusStr := "" - for _, flag := range orderedChanStatusFlags { - if c&flag == flag { - statusStr += chanStatusStrings[flag] + "|" - c -= flag - } - } - - // Remove anything to the right of the final bar, including it as well. - statusStr = strings.TrimRight(statusStr, "|") - - // Add any remaining flags which aren't accounted for as hex. - if c != 0 { - statusStr += "|0x" + strconv.FormatUint(uint64(c), 16) - } - - // If this was purely an unknown flag, then remove the extra bar at the - // start of the string. - statusStr = strings.TrimLeft(statusStr, "|") - - return statusStr -} - // FinalHtlcByte defines a byte type that encodes information about the final // htlc resolution. type FinalHtlcByte byte @@ -3653,15 +3496,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, } // FinalHtlcInfo contains information about the final outcome of an htlc. -type FinalHtlcInfo struct { - // Settled is true is the htlc was settled. If false, the htlc was - // failed. - Settled bool - - // Offchain indicates whether the htlc was resolved off-chain or - // on-chain. - Offchain bool -} +type FinalHtlcInfo = cstate.FinalHtlcInfo // putFinalHtlc writes the final htlc outcome to the database. Additionally it // records whether the htlc was resolved off-chain or on-chain. @@ -3909,122 +3744,39 @@ func (c *OpenChannel) FindPreviousState( return rl, commit, nil } -// ClosureType is an enum like structure that details exactly _how_ a channel -// was closed. Three closure types are currently possible: none, cooperative, -// local force close, remote force close, and (remote) breach. -type ClosureType uint8 +// ClosureType is an enum like structure that details exactly how a channel was +// closed. +type ClosureType = cstate.ClosureType const ( // CooperativeClose indicates that a channel has been closed - // cooperatively. This means that both channel peers were online and - // signed a new transaction paying out the settled balance of the - // contract. - CooperativeClose ClosureType = 0 + // cooperatively. + CooperativeClose = cstate.CooperativeClose // LocalForceClose indicates that we have unilaterally broadcast our // current commitment state on-chain. - LocalForceClose ClosureType = 1 + LocalForceClose = cstate.LocalForceClose // RemoteForceClose indicates that the remote peer has unilaterally // broadcast their current commitment state on-chain. - RemoteForceClose ClosureType = 4 + RemoteForceClose = cstate.RemoteForceClose // BreachClose indicates that the remote peer attempted to broadcast a - // prior _revoked_ channel state. - BreachClose ClosureType = 2 + // prior revoked channel state. + BreachClose = cstate.BreachClose // FundingCanceled indicates that the channel never was fully opened - // before it was marked as closed in the database. This can happen if - // we or the remote fail at some point during the opening workflow, or - // we timeout waiting for the funding transaction to be confirmed. - FundingCanceled ClosureType = 3 - - // Abandoned indicates that the channel state was removed without - // any further actions. This is intended to clean up unusable - // channels during development. - Abandoned ClosureType = 5 + // before it was marked as closed in the database. + FundingCanceled = cstate.FundingCanceled + + // Abandoned indicates that the channel state was removed without any + // further actions. + Abandoned = cstate.Abandoned ) // ChannelCloseSummary contains the final state of a channel at the point it -// was closed. Once a channel is closed, all the information pertaining to that -// channel within the openChannelBucket is deleted, and a compact summary is -// put in place instead. -type ChannelCloseSummary struct { - // ChanPoint is the outpoint for this channel's funding transaction, - // and is used as a unique identifier for the channel. - ChanPoint wire.OutPoint - - // ShortChanID encodes the exact location in the chain in which the - // channel was initially confirmed. This includes: the block height, - // transaction index, and the output within the target transaction. - ShortChanID lnwire.ShortChannelID - - // ChainHash is the hash of the genesis block that this channel resides - // within. - ChainHash chainhash.Hash - - // ClosingTXID is the txid of the transaction which ultimately closed - // this channel. - ClosingTXID chainhash.Hash - - // RemotePub is the public key of the remote peer that we formerly had - // a channel with. - RemotePub *btcec.PublicKey - - // Capacity was the total capacity of the channel. - Capacity btcutil.Amount - - // CloseHeight is the height at which the funding transaction was - // spent. - CloseHeight uint32 - - // SettledBalance is our total balance settled balance at the time of - // channel closure. This _does not_ include the sum of any outputs that - // have been time-locked as a result of the unilateral channel closure. - SettledBalance btcutil.Amount - - // TimeLockedBalance is the sum of all the time-locked outputs at the - // time of channel closure. If we triggered the force closure of this - // channel, then this value will be non-zero if our settled output is - // above the dust limit. If we were on the receiving side of a channel - // force closure, then this value will be non-zero if we had any - // outstanding outgoing HTLC's at the time of channel closure. - TimeLockedBalance btcutil.Amount - - // CloseType details exactly _how_ the channel was closed. Five closure - // types are possible: cooperative, local force, remote force, breach - // and funding canceled. - CloseType ClosureType - - // IsPending indicates whether this channel is in the 'pending close' - // state, which means the channel closing transaction has been - // confirmed, but not yet been fully resolved. In the case of a channel - // that has been cooperatively closed, it will go straight into the - // fully resolved state as soon as the closing transaction has been - // confirmed. However, for channels that have been force closed, they'll - // stay marked as "pending" until _all_ the pending funds have been - // swept. - IsPending bool - - // RemoteCurrentRevocation is the current revocation for their - // commitment transaction. However, since this is the derived public key, - // we don't yet have the private key so we aren't yet able to verify - // that it's actually in the hash chain. - RemoteCurrentRevocation *btcec.PublicKey - - // RemoteNextRevocation is the revocation key to be used for the *next* - // commitment transaction we create for the local node. Within the - // specification, this value is referred to as the - // per-commitment-point. - RemoteNextRevocation *btcec.PublicKey - - // LocalChanConfig is the channel configuration for the local node. - LocalChanConfig ChannelConfig - - // LastChanSyncMsg is the ChannelReestablish message for this channel - // for the state at the point where it was closed. - LastChanSyncMsg *lnwire.ChannelReestablish -} +// was closed. +type ChannelCloseSummary = cstate.ChannelCloseSummary // CloseChannel closes a previously active Lightning channel. Closing a // channel entails persisting a record of the close while either purging the diff --git a/channeldb/chanstate_assertions.go b/channeldb/chanstate_assertions.go new file mode 100644 index 0000000000..453c02f04f --- /dev/null +++ b/channeldb/chanstate_assertions.go @@ -0,0 +1,7 @@ +package channeldb + +import "github.com/lightningnetwork/lnd/chanstate" + +// Compile-time assertion that ChannelStateDB satisfies the channel-state store +// contract while the KV implementation still lives in channeldb. +var _ chanstate.Store[*OpenChannel] = (*ChannelStateDB)(nil) diff --git a/channeldb/db.go b/channeldb/db.go index 3f0036b112..b76c0d61f0 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -32,6 +32,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/migration34" "github.com/lightningnetwork/lnd/channeldb/migration35" "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" + "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/clock" graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/invoices" @@ -777,10 +778,7 @@ func (c *ChannelStateDB) FetchChannelByID(id lnwire.ChannelID) (*OpenChannel, } // ChanCount is used by the server in determining access control. -type ChanCount struct { - HasOpenOrClosedChan bool - PendingOpenCount uint64 -} +type ChanCount = chanstate.ChanCount // 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 @@ -1678,17 +1676,8 @@ func (c *ChannelStateDB) RepairLinkNodes(network wire.BitcoinNet) error { } // ChannelShell is a shell of a channel that is meant to be used for channel -// recovery purposes. It contains a minimal OpenChannel instance along with -// addresses for that target node. -type ChannelShell struct { - // NodeAddrs the set of addresses that this node has known to be - // reachable at in the past. - NodeAddrs []net.Addr - - // Chan is a shell of an OpenChannel, it contains only the items - // required to restore the channel on disk. - Chan *OpenChannel -} +// recovery purposes. +type ChannelShell = chanstate.ChannelShell[*OpenChannel] // RestoreChannelShells is a method that allows the caller to reconstruct the // state of an OpenChannel from the ChannelShell. We'll attempt to write the diff --git a/channelnotifier/channelnotifier.go b/channelnotifier/channelnotifier.go index 06f3e67c0c..7648cc8c0a 100644 --- a/channelnotifier/channelnotifier.go +++ b/channelnotifier/channelnotifier.go @@ -18,7 +18,7 @@ type ChannelNotifier struct { ntfnServer *subscribe.Server - chanDB chanstate.Store + chanDB chanstate.Store[*channeldb.OpenChannel] } // PendingOpenChannelEvent represents a new event where a new channel has @@ -98,7 +98,7 @@ type FundingTimeoutEvent struct { // New creates a new channel notifier. The ChannelNotifier gets channel // events from peers and from the chain arbitrator, and dispatches them to // its clients. -func New(chanDB chanstate.Store) *ChannelNotifier { +func New(chanDB chanstate.Store[*channeldb.OpenChannel]) *ChannelNotifier { return &ChannelNotifier{ ntfnServer: subscribe.NewServer(), chanDB: chanDB, diff --git a/chanrestore.go b/chanrestore.go index 407cdfbc7a..129dbd34e1 100644 --- a/chanrestore.go +++ b/chanrestore.go @@ -36,7 +36,7 @@ const ( // need the secret key chain in order obtain the prior shachain root so we can // verify the DLP protocol as initiated by the remote node. type chanDBRestorer struct { - db chanstate.OpenChannelStore + db chanstate.OpenChannelStore[*channeldb.OpenChannel] secretKeys keychain.SecretKeyRing diff --git a/chanstate/channel.go b/chanstate/channel.go new file mode 100644 index 0000000000..0950f4c729 --- /dev/null +++ b/chanstate/channel.go @@ -0,0 +1,18 @@ +package chanstate + +// ChanCount is used by the server in determining access control. +type ChanCount struct { + HasOpenOrClosedChan bool + PendingOpenCount uint64 +} + +// FinalHtlcInfo contains information about the final outcome of an htlc. +type FinalHtlcInfo struct { + // Settled is true is the htlc was settled. If false, the htlc was + // failed. + Settled bool + + // Offchain indicates whether the htlc was resolved off-chain or + // on-chain. + Offchain bool +} diff --git a/chanstate/channel_status.go b/chanstate/channel_status.go new file mode 100644 index 0000000000..b19fe3659e --- /dev/null +++ b/chanstate/channel_status.go @@ -0,0 +1,110 @@ +package chanstate + +import ( + "strconv" + "strings" +) + +// 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 uint64 + +var ( + // ChanStatusDefault is the normal state of an open channel. + ChanStatusDefault ChannelStatus + + // ChanStatusBorked indicates that the channel has entered an + // irreconcilable state, triggered by a state desynchronization or + // channel breach. Channels in this state should never be added to the + // htlc switch. + ChanStatusBorked ChannelStatus = 1 + + // ChanStatusCommitBroadcasted indicates that a commitment for this + // channel has been broadcasted. + ChanStatusCommitBroadcasted ChannelStatus = 1 << 1 + + // ChanStatusLocalDataLoss indicates that we have lost channel state + // for this channel, and broadcasting our latest commitment might be + // considered a breach. + // + // TODO(halseh): actually enforce that we are not force closing such a + // channel. + ChanStatusLocalDataLoss ChannelStatus = 1 << 2 + + // ChanStatusRestored is a status flag that signals that the channel + // has been restored, and doesn't have all the fields a typical channel + // will have. + ChanStatusRestored ChannelStatus = 1 << 3 + + // ChanStatusCoopBroadcasted indicates that a cooperative close for + // this channel has been broadcasted. Older cooperatively closed + // channels will only have this status set. Newer ones will also have + // close initiator information stored using the local/remote initiator + // status. This status is set in conjunction with the initiator status + // so that we do not need to check multiple channel statues for + // cooperative closes. + ChanStatusCoopBroadcasted ChannelStatus = 1 << 4 + + // ChanStatusLocalCloseInitiator indicates that we initiated closing + // the channel. + ChanStatusLocalCloseInitiator ChannelStatus = 1 << 5 + + // ChanStatusRemoteCloseInitiator indicates that the remote node + // initiated closing the channel. + ChanStatusRemoteCloseInitiator ChannelStatus = 1 << 6 +) + +// chanStatusStrings maps a ChannelStatus to a human friendly string that +// describes that status. +var chanStatusStrings = map[ChannelStatus]string{ + ChanStatusDefault: "ChanStatusDefault", + ChanStatusBorked: "ChanStatusBorked", + ChanStatusCommitBroadcasted: "ChanStatusCommitBroadcasted", + ChanStatusLocalDataLoss: "ChanStatusLocalDataLoss", + ChanStatusRestored: "ChanStatusRestored", + ChanStatusCoopBroadcasted: "ChanStatusCoopBroadcasted", + ChanStatusLocalCloseInitiator: "ChanStatusLocalCloseInitiator", + ChanStatusRemoteCloseInitiator: "ChanStatusRemoteCloseInitiator", +} + +// orderedChanStatusFlags is an in-order list of all that channel status flags. +var orderedChanStatusFlags = []ChannelStatus{ + ChanStatusBorked, + ChanStatusCommitBroadcasted, + ChanStatusLocalDataLoss, + ChanStatusRestored, + ChanStatusCoopBroadcasted, + ChanStatusLocalCloseInitiator, + ChanStatusRemoteCloseInitiator, +} + +// String returns a human-readable representation of the ChannelStatus. +func (c ChannelStatus) String() string { + // If no flags are set, then this is the default case. + if c == ChanStatusDefault { + return chanStatusStrings[ChanStatusDefault] + } + + // Add individual bit flags. + statusStr := "" + for _, flag := range orderedChanStatusFlags { + if c&flag == flag { + statusStr += chanStatusStrings[flag] + "|" + c -= flag + } + } + + // Remove anything to the right of the final bar, including it as well. + statusStr = strings.TrimRight(statusStr, "|") + + // Add any remaining flags which aren't accounted for as hex. + if c != 0 { + statusStr += "|0x" + strconv.FormatUint(uint64(c), 16) + } + + // If this was purely an unknown flag, then remove the extra bar at the + // start of the string. + statusStr = strings.TrimLeft(statusStr, "|") + + return statusStr +} diff --git a/chanstate/close_summary.go b/chanstate/close_summary.go new file mode 100644 index 0000000000..779a4c638f --- /dev/null +++ b/chanstate/close_summary.go @@ -0,0 +1,126 @@ +package chanstate + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnwire" +) + +// ClosureType is an enum like structure that details exactly _how_ a channel +// was closed. Three closure types are currently possible: none, cooperative, +// local force close, remote force close, and (remote) breach. +type ClosureType uint8 + +const ( + // CooperativeClose indicates that a channel has been closed + // cooperatively. This means that both channel peers were online and + // signed a new transaction paying out the settled balance of the + // contract. + CooperativeClose ClosureType = 0 + + // LocalForceClose indicates that we have unilaterally broadcast our + // current commitment state on-chain. + LocalForceClose ClosureType = 1 + + // RemoteForceClose indicates that the remote peer has unilaterally + // broadcast their current commitment state on-chain. + RemoteForceClose ClosureType = 4 + + // BreachClose indicates that the remote peer attempted to broadcast a + // prior _revoked_ channel state. + BreachClose ClosureType = 2 + + // FundingCanceled indicates that the channel never was fully opened + // before it was marked as closed in the database. This can happen if + // we or the remote fail at some point during the opening workflow, or + // we timeout waiting for the funding transaction to be confirmed. + FundingCanceled ClosureType = 3 + + // Abandoned indicates that the channel state was removed without + // any further actions. This is intended to clean up unusable + // channels during development. + Abandoned ClosureType = 5 +) + +// ChannelCloseSummary contains the final state of a channel at the point it +// was closed. Once a channel is closed, all the information pertaining to that +// channel within the openChannelBucket is deleted, and a compact summary is +// put in place instead. +type ChannelCloseSummary struct { + // ChanPoint is the outpoint for this channel's funding transaction, + // and is used as a unique identifier for the channel. + ChanPoint wire.OutPoint + + // ShortChanID encodes the exact location in the chain in which the + // channel was initially confirmed. This includes: the block height, + // transaction index, and the output within the target transaction. + ShortChanID lnwire.ShortChannelID + + // ChainHash is the hash of the genesis block that this channel resides + // within. + ChainHash chainhash.Hash + + // ClosingTXID is the txid of the transaction which ultimately closed + // this channel. + ClosingTXID chainhash.Hash + + // RemotePub is the public key of the remote peer that we formerly had + // a channel with. + RemotePub *btcec.PublicKey + + // Capacity was the total capacity of the channel. + Capacity btcutil.Amount + + // CloseHeight is the height at which the funding transaction was + // spent. + CloseHeight uint32 + + // SettledBalance is our total balance settled balance at the time of + // channel closure. This _does not_ include the sum of any outputs that + // have been time-locked as a result of the unilateral channel closure. + SettledBalance btcutil.Amount + + // TimeLockedBalance is the sum of all the time-locked outputs at the + // time of channel closure. If we triggered the force closure of this + // channel, then this value will be non-zero if our settled output is + // above the dust limit. If we were on the receiving side of a channel + // force closure, then this value will be non-zero if we had any + // outstanding outgoing HTLC's at the time of channel closure. + TimeLockedBalance btcutil.Amount + + // CloseType details exactly _how_ the channel was closed. Five closure + // types are possible: cooperative, local force, remote force, breach + // and funding canceled. + CloseType ClosureType + + // IsPending indicates whether this channel is in the 'pending close' + // state, which means the channel closing transaction has been + // confirmed, but not yet been fully resolved. In the case of a channel + // that has been cooperatively closed, it will go straight into the + // fully resolved state as soon as the closing transaction has been + // confirmed. However, for channels that have been force closed, they'll + // stay marked as "pending" until _all_ the pending funds have been + // swept. + IsPending bool + + // RemoteCurrentRevocation is the current revocation for their + // commitment transaction. However, since this is the derived public + // key, we don't yet have the private key so we aren't yet able to + // verify that it's actually in the hash chain. + RemoteCurrentRevocation *btcec.PublicKey + + // RemoteNextRevocation is the revocation key to be used for the *next* + // commitment transaction we create for the local node. Within the + // specification, this value is referred to as the + // per-commitment-point. + RemoteNextRevocation *btcec.PublicKey + + // LocalChanConfig is the channel configuration for the local node. + LocalChanConfig ChannelConfig + + // LastChanSyncMsg is the ChannelReestablish message for this channel + // for the state at the point where it was closed. + LastChanSyncMsg *lnwire.ChannelReestablish +} diff --git a/chanstate/config.go b/chanstate/config.go new file mode 100644 index 0000000000..17e3e5e4fa --- /dev/null +++ b/chanstate/config.go @@ -0,0 +1,108 @@ +package chanstate + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwire" +) + +// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel +// that are responsible for providing bounds on the state space of the abstract +// channel state. These values must be remembered for normal channel operation +// but they do not impact how we compute the commitment transactions themselves. +type ChannelStateBounds struct { + // ChanReserve is an absolute reservation on the channel for the + // owner of this set of constraints. This means that the current + // settled balance for this node CANNOT dip below the reservation + // amount. This acts as a defense against costless attacks when + // either side no longer has any skin in the game. + ChanReserve btcutil.Amount + + // MaxPendingAmount is the maximum pending HTLC value that the + // owner of these constraints can offer the remote node at a + // particular time. + MaxPendingAmount lnwire.MilliSatoshi + + // MinHTLC is the minimum HTLC value that the owner of these + // constraints can offer the remote node. If any HTLCs below this + // amount are offered, then the HTLC will be rejected. This, in + // tandem with the dust limit allows a node to regulate the + // smallest HTLC that it deems economically relevant. + MinHTLC lnwire.MilliSatoshi + + // MaxAcceptedHtlcs is the maximum number of HTLCs that the owner of + // this set of constraints can offer the remote node. This allows each + // node to limit their over all exposure to HTLCs that may need to be + // acted upon in the case of a unilateral channel closure or a contract + // breach. + MaxAcceptedHtlcs uint16 +} + +// CommitmentParams are the parameters from OpenChannel and +// AcceptChannel that are required to render an abstract channel state to a +// concrete commitment transaction. These values are necessary to (re)compute +// the commitment transaction. We treat these differently than the state space +// bounds because their history needs to be stored in order to properly handle +// chain resolution. +type CommitmentParams struct { + // DustLimit is the threshold (in satoshis) below which any outputs + // should be trimmed. When an output is trimmed, it isn't materialized + // as an actual output, but is instead burned to miner's fees. + DustLimit btcutil.Amount + + // CsvDelay is the relative time lock delay expressed in blocks. Any + // settled outputs that pay to the owner of this channel configuration + // MUST ensure that the delay branch uses this value as the relative + // time lock. Similarly, any HTLC's offered by this node should use + // this value as well. + CsvDelay uint16 +} + +// ChannelConfig is a struct that houses the various configuration opens for +// channels. Each side maintains an instance of this configuration file as it +// governs: how the funding and commitment transaction to be created, the +// nature of HTLC's allotted, the keys to be used for delivery, and relative +// time lock parameters. +type ChannelConfig struct { + // ChannelStateBounds is the set of constraints that must be + // upheld for the duration of the channel for the owner of this channel + // configuration. Constraints govern a number of flow control related + // parameters, also including the smallest HTLC that will be accepted + // by a participant. + ChannelStateBounds + + // CommitmentParams is an embedding of the parameters + // required to render an abstract channel state into a concrete + // commitment transaction. + CommitmentParams + + // MultiSigKey is the key to be used within the 2-of-2 output script + // for the owner of this channel config. + MultiSigKey keychain.KeyDescriptor + + // RevocationBasePoint is the base public key to be used when deriving + // revocation keys for the remote node's commitment transaction. This + // will be combined along with a per commitment secret to derive a + // unique revocation key for each state. + RevocationBasePoint keychain.KeyDescriptor + + // PaymentBasePoint is the base public key to be used when deriving + // the key used within the non-delayed pay-to-self output on the + // commitment transaction for a node. This will be combined with a + // tweak derived from the per-commitment point to ensure unique keys + // for each commitment transaction. + PaymentBasePoint keychain.KeyDescriptor + + // DelayBasePoint is the base public key to be used when deriving the + // key used within the delayed pay-to-self output on the commitment + // transaction for a node. This will be combined with a tweak derived + // from the per-commitment point to ensure unique keys for each + // commitment transaction. + DelayBasePoint keychain.KeyDescriptor + + // HtlcBasePoint is the base public key to be used when deriving the + // local HTLC key. The derived key (combined with the tweak derived + // from the per-commitment point) is used within the "to self" clause + // within any HTLC output scripts. + HtlcBasePoint keychain.KeyDescriptor +} diff --git a/chanstate/interface.go b/chanstate/interface.go index 4934485512..c2c931b39c 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -3,7 +3,6 @@ package chanstate import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lnwire" ) @@ -17,16 +16,16 @@ import ( // concrete channeldb.ChannelStateDB type during the migration. Once the channel // state implementation moves into this package and the old concrete type is no // longer part of consumer-facing code, this name can be revisited. -type Store interface { +type Store[Channel any] interface { // OpenChannelStore owns open-channel records. - OpenChannelStore + OpenChannelStore[Channel] // HistoricalChannelStore owns the post-close historical channel view. - HistoricalChannelStore + HistoricalChannelStore[Channel] // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. - ClosedChannelStore + ClosedChannelStore[Channel] // FinalHTLCStore owns final HTLC outcome data. FinalHTLCStore @@ -41,54 +40,52 @@ type Store interface { } // OpenChannelStore owns open-channel records. -type OpenChannelStore interface { +type OpenChannelStore[Channel any] interface { // 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. - FetchOpenChannels(nodeID *btcec.PublicKey) ( - []*channeldb.OpenChannel, error) + FetchOpenChannels(nodeID *btcec.PublicKey) ([]Channel, error) // FetchChannel attempts to locate a channel specified by the passed // channel point. If the channel cannot be found, then an error will // be returned. - FetchChannel(chanPoint wire.OutPoint) (*channeldb.OpenChannel, error) + FetchChannel(chanPoint wire.OutPoint) (Channel, error) // FetchChannelByID attempts to locate a channel specified by the // passed channel ID. If the channel cannot be found, then an error // will be returned. - FetchChannelByID(id lnwire.ChannelID) (*channeldb.OpenChannel, error) + FetchChannelByID(id lnwire.ChannelID) (Channel, error) // 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. - FetchAllChannels() ([]*channeldb.OpenChannel, error) + FetchAllChannels() ([]Channel, error) // FetchAllOpenChannels will return all channels that have the // funding transaction confirmed, and is not waiting for a closing // transaction to be confirmed. - FetchAllOpenChannels() ([]*channeldb.OpenChannel, error) + FetchAllOpenChannels() ([]Channel, error) // 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. - FetchPendingChannels() ([]*channeldb.OpenChannel, error) + FetchPendingChannels() ([]Channel, error) // 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. - FetchWaitingCloseChannels() ([]*channeldb.OpenChannel, error) + FetchWaitingCloseChannels() ([]Channel, error) // 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. - FetchPermAndTempPeers(chainHash []byte) ( - map[string]channeldb.ChanCount, error) + FetchPermAndTempPeers(chainHash []byte) (map[string]ChanCount, error) // RestoreChannelShells reconstructs the state of an OpenChannel from // the ChannelShell. We'll attempt to write the new channel to disk, @@ -96,19 +93,18 @@ type OpenChannelStore interface { // finally create an edge within the graph for the channel as well. // This method is idempotent, so repeated calls with the same set of // channel shells won't modify the database after the initial call. - RestoreChannelShells(channelShells ...*channeldb.ChannelShell) error + RestoreChannelShells(channelShells ...*ChannelShell[Channel]) error } // HistoricalChannelStore owns the post-close historical channel view. -type HistoricalChannelStore interface { +type HistoricalChannelStore[Channel any] interface { // FetchHistoricalChannel fetches open channel data from the // historical channel bucket. - FetchHistoricalChannel(outPoint *wire.OutPoint) ( - *channeldb.OpenChannel, error) + FetchHistoricalChannel(outPoint *wire.OutPoint) (Channel, error) } // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. -type ClosedChannelStore interface { +type ClosedChannelStore[Channel any] interface { // 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 @@ -117,17 +113,17 @@ type ClosedChannelStore interface { // become fully closed after _all_ the pending funds (if any) have // been swept. FetchClosedChannels(pendingOnly bool) ( - []*channeldb.ChannelCloseSummary, error) + []*ChannelCloseSummary, error) // FetchClosedChannel queries for a channel close summary using the // channel point of the channel in question. FetchClosedChannel(chanID *wire.OutPoint) ( - *channeldb.ChannelCloseSummary, error) + *ChannelCloseSummary, error) // FetchClosedChannelForID queries for a channel close summary using // the channel ID of the channel in question. FetchClosedChannelForID(cid lnwire.ChannelID) ( - *channeldb.ChannelCloseSummary, error) + *ChannelCloseSummary, error) // MarkChanFullyClosed marks a channel as fully closed within the // database. A channel should be marked as fully closed if the @@ -142,9 +138,8 @@ type ClosedChannelStore interface { // FetchClosedChannel and FetchClosedChannelForID. Any ChannelStatus // values are merged into the archived summary. Returns // ErrChannelCloseSummaryNil if summary is nil. - CloseChannel(channel *channeldb.OpenChannel, - summary *channeldb.ChannelCloseSummary, - statuses ...channeldb.ChannelStatus) error + CloseChannel(channel Channel, summary *ChannelCloseSummary, + statuses ...ChannelStatus) error // AbandonChannel attempts to remove the target channel from the open // channel database. If the channel was already removed (has a closed @@ -159,7 +154,7 @@ type FinalHTLCStore interface { // database. If the htlc has no final resolution yet, ErrHtlcUnknown // is returned. LookupFinalHtlc(chanID lnwire.ShortChannelID, - htlcIndex uint64) (*channeldb.FinalHtlcInfo, error) + htlcIndex uint64) (*FinalHtlcInfo, error) // PutOnchainFinalHtlcOutcome stores the final on-chain outcome of an // htlc in the database. @@ -211,18 +206,3 @@ type LinkNodeMaintainer interface { // called on startup to ensure that our database is consistent. RepairLinkNodes(network wire.BitcoinNet) error } - -// Compile-time assertion that channeldb.ChannelStateDB satisfies the Store -// contract. If a method signature drifts on the concrete type, -// this assertion will fail to build before any consumer migration. -// -// NOTE: This assertion lives in the interface file as a temporary exception to -// the established pattern (see invoices/sql_store.go, payments/db/kv_store.go, -// graph/db/kv_store.go), where each implementation asserts itself in its own -// file. The implementation still lives in channeldb/, and channeldb must not -// import chanstate to avoid a cycle, so the assertion has no local -// implementation file to live in yet. When the KV implementation moves into -// this package (chanstate/kv_store.go), this assertion MUST be removed from -// here and re-stated next to the local implementation, matching the precedent -// packages. -var _ Store = (*channeldb.ChannelStateDB)(nil) diff --git a/chanstate/open_channel_types.go b/chanstate/open_channel_types.go new file mode 100644 index 0000000000..0a3e279b29 --- /dev/null +++ b/chanstate/open_channel_types.go @@ -0,0 +1,16 @@ +package chanstate + +import "net" + +// ChannelShell is a shell of a channel that is meant to be used for channel +// recovery purposes. It contains a minimal OpenChannel instance along with +// addresses for that target node. +type ChannelShell[Channel any] struct { + // NodeAddrs the set of addresses that this node has known to be + // reachable at in the past. + NodeAddrs []net.Addr + + // Chan is a shell of an OpenChannel, it contains only the items + // required to restore the channel on disk. + Chan Channel +} diff --git a/contractcourt/breach_arbitrator.go b/contractcourt/breach_arbitrator.go index 2c12f25598..9d00540a5c 100644 --- a/contractcourt/breach_arbitrator.go +++ b/contractcourt/breach_arbitrator.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" @@ -142,7 +143,7 @@ type BreachConfig struct { // DB provides access to the user's closed channels, allowing the breach // arbiter to determine how it should respond to channel closure. - DB chanstate.ClosedChannelStore + DB chanstate.ClosedChannelStore[*channeldb.OpenChannel] // Estimator is used by the breach arbiter to determine an appropriate // fee level when generating, signing, and broadcasting sweep diff --git a/funding/manager.go b/funding/manager.go index 2dcd4f1b73..d1b319c5d4 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -387,7 +387,7 @@ type Config struct { // ChannelDB is the database that keeps track of channel state used by // the funding flow. - ChannelDB chanstate.Store + ChannelDB chanstate.Store[*channeldb.OpenChannel] // SignMessage signs an arbitrary message with a given public key. The // actual digest signed is the double sha-256 of the message. In the diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index b4d39a99c5..54735e8680 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -72,7 +72,7 @@ type AddInvoiceConfig struct { DefaultCLTVExpiry uint32 // ChanDB is used to access open channel state. - ChanDB chanstate.OpenChannelStore + ChanDB chanstate.OpenChannelStore[*channeldb.OpenChannel] // Graph gives the invoice server access to various graph related // queries. diff --git a/lnrpc/invoicesrpc/config_active.go b/lnrpc/invoicesrpc/config_active.go index 233aa59275..bb20d173a9 100644 --- a/lnrpc/invoicesrpc/config_active.go +++ b/lnrpc/invoicesrpc/config_active.go @@ -5,6 +5,7 @@ package invoicesrpc import ( "github.com/btcsuite/btcd/chaincfg" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnwire" @@ -57,7 +58,7 @@ type Config struct { // ChanStateDB is a possibly replicated db instance which contains open // channel state. - ChanStateDB chanstate.OpenChannelStore + ChanStateDB chanstate.OpenChannelStore[*channeldb.OpenChannel] // GenInvoiceFeatures returns a feature containing feature bits that // should be advertised on freshly generated invoices. diff --git a/lnrpc/walletrpc/config_active.go b/lnrpc/walletrpc/config_active.go index e0c9c684a4..97bbb6411c 100644 --- a/lnrpc/walletrpc/config_active.go +++ b/lnrpc/walletrpc/config_active.go @@ -6,6 +6,7 @@ package walletrpc import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcwallet/wallet" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -79,5 +80,5 @@ type Config struct { CoinSelectionStrategy wallet.CoinSelectionStrategy // ChanStateDB is the reference to the open channel store. - ChanStateDB chanstate.OpenChannelStore + ChanStateDB chanstate.OpenChannelStore[*channeldb.OpenChannel] } diff --git a/peer/brontide.go b/peer/brontide.go index f7a01cd11f..6f95032932 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -261,7 +261,7 @@ type Config struct { InterceptSwitch *htlcswitch.InterceptableSwitch // ChannelDB is used to fetch channel state needed by the peer. - ChannelDB chanstate.Store + ChannelDB chanstate.Store[*channeldb.OpenChannel] // ChannelGraph is a pointer to the channel graph which is used to // query information about the set of known active channels. diff --git a/server.go b/server.go index 45992c464c..b49600ff8a 100644 --- a/server.go +++ b/server.go @@ -326,7 +326,7 @@ type server struct { graphDB *graphdb.ChannelGraph v1Graph *graphdb.VersionedGraph - chanStateDB chanstate.Store + chanStateDB chanstate.Store[*channeldb.OpenChannel] linkNodeDB *channeldb.LinkNodeDB addrSource channeldb.AddrSource diff --git a/subrpcserver_config.go b/subrpcserver_config.go index 856553c38f..efb71f7180 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -11,6 +11,7 @@ import ( "github.com/lightningnetwork/lnd/aliasmgr" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" @@ -115,7 +116,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, routerBackend *routerrpc.RouterBackend, nodeSigner *netann.NodeSigner, graphDB *graphdb.ChannelGraph, - chanStateDB chanstate.Store, + chanStateDB chanstate.Store[*channeldb.OpenChannel], sweeper *sweep.UtxoSweeper, tower *watchtower.Standalone, towerClientMgr *wtclient.Manager, From 6d349cab35c8d79433c205b97d39d2083ab83c5c Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 16:56:18 -0300 Subject: [PATCH 02/33] chanstate: move channel type flags Move ChannelType and its flag helpers into chanstate while leaving compatibility aliases in channeldb. This is a backend-neutral value type and does not require moving any KV serialization logic. Keep the full type documentation with the moved chanstate definition. The channeldb aliases preserve the existing public surface while later commits continue moving OpenChannel state out of the KV package. --- channeldb/channel.go | 140 ++++++--------------------------- chanstate/channel_type.go | 157 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 117 deletions(-) create mode 100644 chanstate/channel_type.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 7ddbcdd4f0..22d24cf999 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -399,161 +399,67 @@ func isOutpointClosed(opBucket kvdb.RBucket, chanKey []byte) (bool, error) { } // ChannelType is an enum-like type that describes one of several possible -// channel types. Each open channel is associated with a particular type as the -// channel type may determine how higher level operations are conducted such as -// fee negotiation, channel closing, the format of HTLCs, etc. Structure-wise, -// a ChannelType is a bit field, with each bit denoting a modification from the -// base channel type of single funder. -type ChannelType uint64 +// channel types. +type ChannelType = cstate.ChannelType const ( - // NOTE: iota isn't used here for this enum needs to be stable - // long-term as it will be persisted to the database. - // SingleFunderBit represents a channel wherein one party solely funds // the entire capacity of the channel. - SingleFunderBit ChannelType = 0 + SingleFunderBit = cstate.SingleFunderBit // DualFunderBit represents a channel wherein both parties contribute - // funds towards the total capacity of the channel. The channel may be - // funded symmetrically or asymmetrically. - DualFunderBit ChannelType = 1 << 0 + // funds towards the total capacity of the channel. + DualFunderBit = cstate.DualFunderBit // SingleFunderTweaklessBit is similar to the basic SingleFunder channel - // type, but it omits the tweak for one's key in the commitment - // transaction of the remote party. - SingleFunderTweaklessBit ChannelType = 1 << 1 + // type, but it omits the tweak for one's key. + SingleFunderTweaklessBit = cstate.SingleFunderTweaklessBit // NoFundingTxBit denotes if we have the funding transaction locally on - // disk. This bit may be on if the funding transaction was crafted by a - // wallet external to the primary daemon. - NoFundingTxBit ChannelType = 1 << 2 + // disk. + NoFundingTxBit = cstate.NoFundingTxBit // AnchorOutputsBit indicates that the channel makes use of anchor - // outputs to bump the commitment transaction's effective feerate. This - // channel type also uses a delayed to_remote output script. - AnchorOutputsBit ChannelType = 1 << 3 + // outputs to bump the commitment transaction's effective feerate. + AnchorOutputsBit = cstate.AnchorOutputsBit // FrozenBit indicates that the channel is a frozen channel, meaning // that only the responder can decide to cooperatively close the // channel. - FrozenBit ChannelType = 1 << 4 + FrozenBit = cstate.FrozenBit // ZeroHtlcTxFeeBit indicates that the channel should use zero-fee // second-level HTLC transactions. - ZeroHtlcTxFeeBit ChannelType = 1 << 5 + ZeroHtlcTxFeeBit = cstate.ZeroHtlcTxFeeBit // LeaseExpirationBit indicates that the channel has been leased for a - // period of time, constraining every output that pays to the channel - // initiator with an additional CLTV of the lease maturity. - LeaseExpirationBit ChannelType = 1 << 6 + // period of time. + LeaseExpirationBit = cstate.LeaseExpirationBit // ZeroConfBit indicates that the channel is a zero-conf channel. - ZeroConfBit ChannelType = 1 << 7 + ZeroConfBit = cstate.ZeroConfBit // ScidAliasChanBit indicates that the channel has negotiated the // scid-alias channel type. - ScidAliasChanBit ChannelType = 1 << 8 + ScidAliasChanBit = cstate.ScidAliasChanBit // ScidAliasFeatureBit indicates that the scid-alias feature bit was // negotiated during the lifetime of this channel. - ScidAliasFeatureBit ChannelType = 1 << 9 + ScidAliasFeatureBit = cstate.ScidAliasFeatureBit // SimpleTaprootFeatureBit indicates that the simple-taproot-chans // feature bit was negotiated during the lifetime of the channel. - SimpleTaprootFeatureBit ChannelType = 1 << 10 + SimpleTaprootFeatureBit = cstate.SimpleTaprootFeatureBit // TapscriptRootBit indicates that this is a MuSig2 channel with a top - // level tapscript commitment. This MUST be set along with the - // SimpleTaprootFeatureBit. - TapscriptRootBit ChannelType = 1 << 11 + // level tapscript commitment. + TapscriptRootBit = cstate.TapscriptRootBit // TaprootFinalBit indicates that this is a MuSig2 channel using the - // final/production taproot scripts and feature bits 80/81. This MUST - // be set along with the SimpleTaprootFeatureBit. - TaprootFinalBit ChannelType = 1 << 12 + // final/production taproot scripts and feature bits 80/81. + TaprootFinalBit = cstate.TaprootFinalBit ) -// IsSingleFunder returns true if the channel type if one of the known single -// funder variants. -func (c ChannelType) IsSingleFunder() bool { - return c&DualFunderBit == 0 -} - -// IsDualFunder returns true if the ChannelType has the DualFunderBit set. -func (c ChannelType) IsDualFunder() bool { - return c&DualFunderBit == DualFunderBit -} - -// IsTweakless returns true if the target channel uses a commitment that -// doesn't tweak the key for the remote party. -func (c ChannelType) IsTweakless() bool { - return c&SingleFunderTweaklessBit == SingleFunderTweaklessBit -} - -// HasFundingTx returns true if this channel type is one that has a funding -// transaction stored locally. -func (c ChannelType) HasFundingTx() bool { - return c&NoFundingTxBit == 0 -} - -// HasAnchors returns true if this channel type has anchor outputs on its -// commitment. -func (c ChannelType) HasAnchors() bool { - return c&AnchorOutputsBit == AnchorOutputsBit -} - -// ZeroHtlcTxFee returns true if this channel type uses second-level HTLC -// transactions signed with zero-fee. -func (c ChannelType) ZeroHtlcTxFee() bool { - return c&ZeroHtlcTxFeeBit == ZeroHtlcTxFeeBit -} - -// IsFrozen returns true if the channel is considered to be "frozen". A frozen -// channel means that only the responder can initiate a cooperative channel -// closure. -func (c ChannelType) IsFrozen() bool { - return c&FrozenBit == FrozenBit -} - -// HasLeaseExpiration returns true if the channel originated from a lease. -func (c ChannelType) HasLeaseExpiration() bool { - return c&LeaseExpirationBit == LeaseExpirationBit -} - -// HasZeroConf returns true if the channel is a zero-conf channel. -func (c ChannelType) HasZeroConf() bool { - return c&ZeroConfBit == ZeroConfBit -} - -// HasScidAliasChan returns true if the scid-alias channel type was negotiated. -func (c ChannelType) HasScidAliasChan() bool { - return c&ScidAliasChanBit == ScidAliasChanBit -} - -// HasScidAliasFeature returns true if the scid-alias feature bit was -// negotiated during the lifetime of this channel. -func (c ChannelType) HasScidAliasFeature() bool { - return c&ScidAliasFeatureBit == ScidAliasFeatureBit -} - -// IsTaproot returns true if the channel is using taproot features. -func (c ChannelType) IsTaproot() bool { - return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit -} - -// HasTapscriptRoot returns true if the channel is using a top level tapscript -// root commitment. -func (c ChannelType) HasTapscriptRoot() bool { - return c&TapscriptRootBit == TapscriptRootBit -} - -// IsTaprootFinal returns true if the channel is using final/production taproot -// scripts and feature bits. -func (c ChannelType) IsTaprootFinal() bool { - return c&TaprootFinalBit == TaprootFinalBit -} - // ChannelStateBounds are the parameters from OpenChannel and AcceptChannel // that bound the abstract channel state. type ChannelStateBounds = cstate.ChannelStateBounds diff --git a/chanstate/channel_type.go b/chanstate/channel_type.go new file mode 100644 index 0000000000..9666307bef --- /dev/null +++ b/chanstate/channel_type.go @@ -0,0 +1,157 @@ +package chanstate + +// ChannelType is an enum-like type that describes one of several possible +// channel types. Each open channel is associated with a particular type as the +// channel type may determine how higher level operations are conducted such as +// fee negotiation, channel closing, the format of HTLCs, etc. Structure-wise, +// a ChannelType is a bit field, with each bit denoting a modification from the +// base channel type of single funder. +type ChannelType uint64 + +const ( + // NOTE: iota isn't used here for this enum needs to be stable + // long-term as it will be persisted to the database. + + // SingleFunderBit represents a channel wherein one party solely funds + // the entire capacity of the channel. + SingleFunderBit ChannelType = 0 + + // DualFunderBit represents a channel wherein both parties contribute + // funds towards the total capacity of the channel. The channel may be + // funded symmetrically or asymmetrically. + DualFunderBit ChannelType = 1 << 0 + + // SingleFunderTweaklessBit is similar to the basic SingleFunder channel + // type, but it omits the tweak for one's key in the commitment + // transaction of the remote party. + SingleFunderTweaklessBit ChannelType = 1 << 1 + + // NoFundingTxBit denotes if we have the funding transaction locally on + // disk. This bit may be on if the funding transaction was crafted by a + // wallet external to the primary daemon. + NoFundingTxBit ChannelType = 1 << 2 + + // AnchorOutputsBit indicates that the channel makes use of anchor + // outputs to bump the commitment transaction's effective feerate. This + // channel type also uses a delayed to_remote output script. + AnchorOutputsBit ChannelType = 1 << 3 + + // FrozenBit indicates that the channel is a frozen channel, meaning + // that only the responder can decide to cooperatively close the + // channel. + FrozenBit ChannelType = 1 << 4 + + // ZeroHtlcTxFeeBit indicates that the channel should use zero-fee + // second-level HTLC transactions. + ZeroHtlcTxFeeBit ChannelType = 1 << 5 + + // LeaseExpirationBit indicates that the channel has been leased for a + // period of time, constraining every output that pays to the channel + // initiator with an additional CLTV of the lease maturity. + LeaseExpirationBit ChannelType = 1 << 6 + + // ZeroConfBit indicates that the channel is a zero-conf channel. + ZeroConfBit ChannelType = 1 << 7 + + // ScidAliasChanBit indicates that the channel has negotiated the + // scid-alias channel type. + ScidAliasChanBit ChannelType = 1 << 8 + + // ScidAliasFeatureBit indicates that the scid-alias feature bit was + // negotiated during the lifetime of this channel. + ScidAliasFeatureBit ChannelType = 1 << 9 + + // SimpleTaprootFeatureBit indicates that the simple-taproot-chans + // feature bit was negotiated during the lifetime of the channel. + SimpleTaprootFeatureBit ChannelType = 1 << 10 + + // TapscriptRootBit indicates that this is a MuSig2 channel with a top + // level tapscript commitment. This MUST be set along with the + // SimpleTaprootFeatureBit. + TapscriptRootBit ChannelType = 1 << 11 + + // TaprootFinalBit indicates that this is a MuSig2 channel using the + // final/production taproot scripts and feature bits 80/81. This MUST + // be set along with the SimpleTaprootFeatureBit. + TaprootFinalBit ChannelType = 1 << 12 +) + +// IsSingleFunder returns true if the channel type if one of the known single +// funder variants. +func (c ChannelType) IsSingleFunder() bool { + return c&DualFunderBit == 0 +} + +// IsDualFunder returns true if the ChannelType has the DualFunderBit set. +func (c ChannelType) IsDualFunder() bool { + return c&DualFunderBit == DualFunderBit +} + +// IsTweakless returns true if the target channel uses a commitment that +// doesn't tweak the key for the remote party. +func (c ChannelType) IsTweakless() bool { + return c&SingleFunderTweaklessBit == SingleFunderTweaklessBit +} + +// HasFundingTx returns true if this channel type is one that has a funding +// transaction stored locally. +func (c ChannelType) HasFundingTx() bool { + return c&NoFundingTxBit == 0 +} + +// HasAnchors returns true if this channel type has anchor outputs on its +// commitment. +func (c ChannelType) HasAnchors() bool { + return c&AnchorOutputsBit == AnchorOutputsBit +} + +// ZeroHtlcTxFee returns true if this channel type uses second-level HTLC +// transactions signed with zero-fee. +func (c ChannelType) ZeroHtlcTxFee() bool { + return c&ZeroHtlcTxFeeBit == ZeroHtlcTxFeeBit +} + +// IsFrozen returns true if the channel is considered to be "frozen". A frozen +// channel means that only the responder can initiate a cooperative channel +// closure. +func (c ChannelType) IsFrozen() bool { + return c&FrozenBit == FrozenBit +} + +// HasLeaseExpiration returns true if the channel originated from a lease. +func (c ChannelType) HasLeaseExpiration() bool { + return c&LeaseExpirationBit == LeaseExpirationBit +} + +// HasZeroConf returns true if the channel is a zero-conf channel. +func (c ChannelType) HasZeroConf() bool { + return c&ZeroConfBit == ZeroConfBit +} + +// HasScidAliasChan returns true if the scid-alias channel type was negotiated. +func (c ChannelType) HasScidAliasChan() bool { + return c&ScidAliasChanBit == ScidAliasChanBit +} + +// HasScidAliasFeature returns true if the scid-alias feature bit was +// negotiated during the lifetime of this channel. +func (c ChannelType) HasScidAliasFeature() bool { + return c&ScidAliasFeatureBit == ScidAliasFeatureBit +} + +// IsTaproot returns true if the channel is using taproot features. +func (c ChannelType) IsTaproot() bool { + return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit +} + +// HasTapscriptRoot returns true if the channel is using a top level tapscript +// root commitment. +func (c ChannelType) HasTapscriptRoot() bool { + return c&TapscriptRootBit == TapscriptRootBit +} + +// IsTaprootFinal returns true if the channel is using final/production taproot +// scripts and feature bits. +func (c ChannelType) IsTaprootFinal() bool { + return c&TaprootFinalBit == TaprootFinalBit +} From eb2338c1587b37045e8b865c81b3af2e64dc5f19 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 17:01:01 -0300 Subject: [PATCH 03/33] chanstate: move open channel errors Move the OpenChannel error definitions into chanstate and leave channeldb aliases for existing callers. These errors describe channel state behavior rather than a concrete KV bucket layout. Keeping the aliases preserves the public channeldb API while later commits move more OpenChannel state and receiver logic toward chanstate. --- channeldb/channel.go | 23 +++++++++--------- chanstate/errors.go | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 chanstate/errors.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 22d24cf999..f9da7b9c86 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -176,50 +176,49 @@ var ( var ( // ErrNoCommitmentsFound is returned when a channel has not set // commitment states. - ErrNoCommitmentsFound = fmt.Errorf("no commitments found") + ErrNoCommitmentsFound = cstate.ErrNoCommitmentsFound // ErrNoChanInfoFound is returned when a particular channel does not // have any channels state. - ErrNoChanInfoFound = fmt.Errorf("no chan info found") + ErrNoChanInfoFound = cstate.ErrNoChanInfoFound // ErrNoRevocationsFound is returned when revocation state for a // particular channel cannot be found. - ErrNoRevocationsFound = fmt.Errorf("no revocations found") + ErrNoRevocationsFound = cstate.ErrNoRevocationsFound // ErrNoPendingCommit is returned when there is not a pending // commitment for a remote party. A new commitment is written to disk // each time we write a new state in order to be properly fault // tolerant. - ErrNoPendingCommit = fmt.Errorf("no pending commits found") + ErrNoPendingCommit = cstate.ErrNoPendingCommit // ErrNoCommitPoint is returned when no data loss commit point is found // in the database. - ErrNoCommitPoint = fmt.Errorf("no commit point found") + ErrNoCommitPoint = cstate.ErrNoCommitPoint // ErrNoCloseTx is returned when no closing tx is found for a channel // in the state CommitBroadcasted. - ErrNoCloseTx = fmt.Errorf("no closing tx found") + ErrNoCloseTx = cstate.ErrNoCloseTx // ErrNoShutdownInfo is returned when no shutdown info has been // persisted for a channel. - ErrNoShutdownInfo = errors.New("no shutdown info") + ErrNoShutdownInfo = cstate.ErrNoShutdownInfo // ErrNoRestoredChannelMutation is returned when a caller attempts to // mutate a channel that's been recovered. - ErrNoRestoredChannelMutation = fmt.Errorf("cannot mutate restored " + - "channel state") + ErrNoRestoredChannelMutation = cstate.ErrNoRestoredChannelMutation // ErrChanBorked is returned when a caller attempts to mutate a borked // channel. - ErrChanBorked = fmt.Errorf("cannot mutate borked channel") + ErrChanBorked = cstate.ErrChanBorked // ErrMissingIndexEntry is returned when a caller attempts to close a // channel and the outpoint is missing from the index. - ErrMissingIndexEntry = fmt.Errorf("missing outpoint from index") + ErrMissingIndexEntry = cstate.ErrMissingIndexEntry // ErrOnionBlobLength is returned is an onion blob with incorrect // length is read from disk. - ErrOnionBlobLength = errors.New("onion blob < 1366 bytes") + ErrOnionBlobLength = cstate.ErrOnionBlobLength ) const ( diff --git a/chanstate/errors.go b/chanstate/errors.go new file mode 100644 index 0000000000..4e8415cf95 --- /dev/null +++ b/chanstate/errors.go @@ -0,0 +1,55 @@ +package chanstate + +import ( + "errors" + "fmt" +) + +var ( + // ErrNoCommitmentsFound is returned when a channel has not set + // commitment states. + ErrNoCommitmentsFound = fmt.Errorf("no commitments found") + + // ErrNoChanInfoFound is returned when a particular channel does not + // have any channels state. + ErrNoChanInfoFound = fmt.Errorf("no chan info found") + + // ErrNoRevocationsFound is returned when revocation state for a + // particular channel cannot be found. + ErrNoRevocationsFound = fmt.Errorf("no revocations found") + + // ErrNoPendingCommit is returned when there is not a pending + // commitment for a remote party. A new commitment is written to disk + // each time we write a new state in order to be properly fault + // tolerant. + ErrNoPendingCommit = fmt.Errorf("no pending commits found") + + // ErrNoCommitPoint is returned when no data loss commit point is found + // in the database. + ErrNoCommitPoint = fmt.Errorf("no commit point found") + + // ErrNoCloseTx is returned when no closing tx is found for a channel + // in the state CommitBroadcasted. + ErrNoCloseTx = fmt.Errorf("no closing tx found") + + // ErrNoShutdownInfo is returned when no shutdown info has been + // persisted for a channel. + ErrNoShutdownInfo = errors.New("no shutdown info") + + // ErrNoRestoredChannelMutation is returned when a caller attempts to + // mutate a channel that's been recovered. + ErrNoRestoredChannelMutation = fmt.Errorf("cannot mutate restored " + + "channel state") + + // ErrChanBorked is returned when a caller attempts to mutate a borked + // channel. + ErrChanBorked = fmt.Errorf("cannot mutate borked channel") + + // ErrMissingIndexEntry is returned when a caller attempts to close a + // channel and the outpoint is missing from the index. + ErrMissingIndexEntry = fmt.Errorf("missing outpoint from index") + + // ErrOnionBlobLength is returned is an onion blob with incorrect + // length is read from disk. + ErrOnionBlobLength = errors.New("onion blob < 1366 bytes") +) From f76e3e99f1b5df929fc352f46abda60a775fcbbb Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 17:06:48 -0300 Subject: [PATCH 04/33] chanstate: move shutdown metadata Move the ShutdownInfo state type, constructor, and closer helper into chanstate. The type describes channel shutdown state and is not tied to the concrete KV backend. Keep the TLV encode and decode helpers in channeldb for now, since those functions describe the current persisted format. The channeldb constructor remains as a compatibility wrapper. --- channeldb/channel.go | 33 +++++---------------------------- chanstate/shutdown.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 chanstate/shutdown.go diff --git a/channeldb/channel.go b/channeldb/channel.go index f9da7b9c86..89dbc03326 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1846,7 +1846,7 @@ func (c *OpenChannel) MarkShutdownSent(info *ShutdownInfo) error { // shutdownInfoKey. func (c *OpenChannel) storeShutdownInfo(info *ShutdownInfo) error { var b bytes.Buffer - err := info.encode(&b) + err := encodeShutdownInfo(info, &b) if err != nil { return err } @@ -4817,40 +4817,17 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { // ShutdownInfo contains various info about the shutdown initiation of a // channel. -type ShutdownInfo struct { - // DeliveryScript is the address that we have included in any previous - // Shutdown message for a particular channel and so should include in - // any future re-sends of the Shutdown message. - DeliveryScript tlv.RecordT[tlv.TlvType0, lnwire.DeliveryAddress] - - // LocalInitiator is true if we sent a Shutdown message before ever - // receiving a Shutdown message from the remote peer. - LocalInitiator tlv.RecordT[tlv.TlvType1, bool] -} +type ShutdownInfo = cstate.ShutdownInfo // NewShutdownInfo constructs a new ShutdownInfo object. func NewShutdownInfo(deliveryScript lnwire.DeliveryAddress, locallyInitiated bool) *ShutdownInfo { - return &ShutdownInfo{ - DeliveryScript: tlv.NewRecordT[tlv.TlvType0](deliveryScript), - LocalInitiator: tlv.NewPrimitiveRecord[tlv.TlvType1]( - locallyInitiated, - ), - } -} - -// Closer identifies the ChannelParty that initiated the coop-closure process. -func (s ShutdownInfo) Closer() lntypes.ChannelParty { - if s.LocalInitiator.Val { - return lntypes.Local - } - - return lntypes.Remote + return cstate.NewShutdownInfo(deliveryScript, locallyInitiated) } -// encode serialises the ShutdownInfo to the given io.Writer. -func (s *ShutdownInfo) encode(w io.Writer) error { +// 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(), diff --git a/chanstate/shutdown.go b/chanstate/shutdown.go new file mode 100644 index 0000000000..4c1dca31a5 --- /dev/null +++ b/chanstate/shutdown.go @@ -0,0 +1,41 @@ +package chanstate + +import ( + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// ShutdownInfo contains various info about the shutdown initiation of a +// channel. +type ShutdownInfo struct { + // DeliveryScript is the address that we have included in any previous + // Shutdown message for a particular channel and so should include in + // any future re-sends of the Shutdown message. + DeliveryScript tlv.RecordT[tlv.TlvType0, lnwire.DeliveryAddress] + + // LocalInitiator is true if we sent a Shutdown message before ever + // receiving a Shutdown message from the remote peer. + LocalInitiator tlv.RecordT[tlv.TlvType1, bool] +} + +// NewShutdownInfo constructs a new ShutdownInfo object. +func NewShutdownInfo(deliveryScript lnwire.DeliveryAddress, + locallyInitiated bool) *ShutdownInfo { + + return &ShutdownInfo{ + DeliveryScript: tlv.NewRecordT[tlv.TlvType0](deliveryScript), + LocalInitiator: tlv.NewPrimitiveRecord[tlv.TlvType1]( + locallyInitiated, + ), + } +} + +// Closer identifies the ChannelParty that initiated the coop-closure process. +func (s ShutdownInfo) Closer() lntypes.ChannelParty { + if s.LocalInitiator.Val { + return lntypes.Local + } + + return lntypes.Remote +} From 992dcf2492ae60b2e201ca5be44a564e4b58ed6c Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 17:15:18 -0300 Subject: [PATCH 05/33] chanstate: add open channel lifecycle store Add a lifecycle facet to the chanstate Store contract for refresh, confirmation, open-state, and SCID mutations. Implement the facet on ChannelStateDB using the existing KV persistence code. Update the matching OpenChannel receivers to call through the store methods instead of reaching into the ChannelStateDB backend directly. Also convert fullSync into a channeldb helper so that KV-specific code is no longer an OpenChannel receiver. --- channeldb/channel.go | 210 ++++++++++++++++++++++++++--------------- chanstate/interface.go | 37 ++++++++ 2 files changed, 170 insertions(+), 77 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 89dbc03326..55ee699dd9 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1127,9 +1127,16 @@ func (c *OpenChannel) Refresh() error { c.Lock() defer c.Unlock() - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + return c.Db.RefreshChannel(c) +} + +// 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, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err @@ -1137,30 +1144,27 @@ func (c *OpenChannel) Refresh() error { // We'll re-populating the in-memory channel with the info // fetched from disk. - if err := fetchChanInfo(chanBucket, c); err != nil { + 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. - if err := fetchChanCommitments(chanBucket, c); err != nil { + err = fetchChanCommitments(chanBucket, channel) + if err != nil { return fmt.Errorf("unable to fetch chan commitments: "+ "%v", err) } // Also retrieve the current revocation state. - if err := fetchChanRevocationState(chanBucket, c); err != nil { + err = fetchChanRevocationState(chanBucket, channel) + if err != nil { return fmt.Errorf("unable to fetch chan revocations: "+ "%v", err) } return nil }, func() {}) - if err != nil { - return err - } - - return nil } // fetchChanBucket is a helper function that returns the bucket where a @@ -1301,9 +1305,9 @@ func fetchFinalHtlcsBucketRw(tx kvdb.RwTx, return chanBucket, nil } -// fullSync syncs the contents of an OpenChannel while re-using an existing -// database transaction. -func (c *OpenChannel) fullSync(tx kvdb.RwTx) error { +// 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 { @@ -1399,29 +1403,40 @@ func (c *OpenChannel) MarkConfirmationHeight(height uint32) error { c.Lock() defer c.Unlock() - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + if err := c.Db.MarkChannelConfirmationHeight(c, height); err != nil { + return err + } + + c.ConfirmationHeight = height + + return nil +} + +// MarkChannelConfirmationHeight updates the channel's confirmation height once +// the channel opening transaction receives one confirmation. +func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, + height uint32) error { + + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - channel, err := fetchOpenChannel(chanBucket, &c.FundingOutpoint) + diskChannel, err := fetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) if err != nil { return err } - channel.ConfirmationHeight = height - - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } - - c.ConfirmationHeight = height + diskChannel.ConfirmationHeight = height - return nil + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) } // ResetCloseConfirmationHeight clears the channel's close confirmation height @@ -1438,63 +1453,86 @@ func (c *OpenChannel) MarkCloseConfirmationHeight( c.Lock() defer c.Unlock() - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + err := c.Db.MarkChannelCloseConfirmationHeight(c, height) + if err != nil { + return err + } + + c.CloseConfirmationHeight = height + + return nil +} + +// MarkChannelCloseConfirmationHeight updates the channel's close confirmation +// height when the closing transaction is first detected in a block. +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, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - channel, err := fetchOpenChannel(chanBucket, &c.FundingOutpoint) + diskChannel, err := fetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) if err != nil { return err } - channel.CloseConfirmationHeight = height + diskChannel.CloseConfirmationHeight = height - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) +} + +// MarkAsOpen marks a channel as fully open given a locator that uniquely +// describes its location within the chain. +func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error { + c.Lock() + defer c.Unlock() + + if err := c.Db.MarkChannelOpen(c, openLoc); err != nil { return err } - c.CloseConfirmationHeight = height + c.IsPending = false + c.ShortChannelID = openLoc + c.Packager = NewChannelPackager(openLoc) return nil } -// MarkAsOpen marks a channel as fully open given a locator that uniquely +// MarkChannelOpen marks a channel as fully open given a locator that uniquely // describes its location within the chain. -func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error { - c.Lock() - defer c.Unlock() +func (c *ChannelStateDB) MarkChannelOpen(channel *OpenChannel, + openLoc lnwire.ShortChannelID) error { - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - channel, err := fetchOpenChannel(chanBucket, &c.FundingOutpoint) + diskChannel, err := fetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) if err != nil { return err } - channel.IsPending = false - channel.ShortChannelID = openLoc - - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } + diskChannel.IsPending = false + diskChannel.ShortChannelID = openLoc - c.IsPending = false - c.ShortChannelID = openLoc - c.Packager = NewChannelPackager(openLoc) - - return nil + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) } // MarkRealScid marks the zero-conf channel's confirmed ShortChannelID. This @@ -1503,31 +1541,39 @@ func (c *OpenChannel) MarkRealScid(realScid lnwire.ShortChannelID) error { c.Lock() defer c.Unlock() - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + if err := c.Db.MarkChannelRealScid(c, realScid); err != nil { + return err + } + + c.confirmedScid = realScid + + return nil +} + +// 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, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - channel, err := fetchOpenChannel( - chanBucket, &c.FundingOutpoint, + diskChannel, err := fetchOpenChannel( + chanBucket, &channel.FundingOutpoint, ) if err != nil { return err } - channel.confirmedScid = realScid - - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } - - c.confirmedScid = realScid + diskChannel.confirmedScid = realScid - return nil + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) } // MarkScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in-memory and @@ -1536,30 +1582,40 @@ func (c *OpenChannel) MarkScidAliasNegotiated() error { c.Lock() defer c.Unlock() - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + if err := c.Db.MarkChannelScidAliasNegotiated(c); err != nil { + return err + } + + c.ChanType |= ScidAliasFeatureBit + + return nil +} + +// MarkChannelScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in the +// database. +func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( + channel *OpenChannel) error { + + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - channel, err := fetchOpenChannel( - chanBucket, &c.FundingOutpoint, + diskChannel, err := fetchOpenChannel( + chanBucket, &channel.FundingOutpoint, ) if err != nil { return err } - channel.ChanType |= ScidAliasFeatureBit - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } + diskChannel.ChanType |= ScidAliasFeatureBit - c.ChanType |= ScidAliasFeatureBit - - return nil + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) } // MarkDataLoss marks sets the channel status to LocalDataLoss and stores the @@ -2216,7 +2272,7 @@ func (c *OpenChannel) SyncPending(addr net.Addr, pendingHeight uint32) error { // LinkNode (if needed) for the channel peer. func syncNewChannel(tx kvdb.RwTx, c *OpenChannel, addrs []net.Addr) error { // First, sync all the persistent channel state to disk. - if err := c.fullSync(tx); err != nil { + if err := fullSyncOpenChannel(tx, c); err != nil { return err } diff --git a/chanstate/interface.go b/chanstate/interface.go index c2c931b39c..6b7a4af6fc 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -3,6 +3,7 @@ package chanstate import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lnwire" ) @@ -23,6 +24,10 @@ type Store[Channel any] interface { // HistoricalChannelStore owns the post-close historical channel view. HistoricalChannelStore[Channel] + // OpenChannelLifecycleStore owns persisted lifecycle state for open + // channel records. + OpenChannelLifecycleStore[Channel] + // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. ClosedChannelStore[Channel] @@ -103,6 +108,38 @@ type HistoricalChannelStore[Channel any] interface { FetchHistoricalChannel(outPoint *wire.OutPoint) (Channel, error) } +// OpenChannelLifecycleStore owns persisted lifecycle state for open channel +// records. +type OpenChannelLifecycleStore[Channel any] interface { + // RefreshChannel updates the in-memory channel state using the latest + // state observed on disk. + RefreshChannel(channel Channel) error + + // MarkChannelConfirmationHeight updates the channel's confirmation + // height once the channel opening transaction receives one + // confirmation. + MarkChannelConfirmationHeight(channel Channel, height uint32) error + + // MarkChannelCloseConfirmationHeight updates the channel's close + // confirmation height when the closing transaction is first detected + // in a block. + MarkChannelCloseConfirmationHeight(channel Channel, + height fn.Option[uint32]) error + + // MarkChannelOpen marks a channel as fully open given a locator that + // uniquely describes its location within the chain. + MarkChannelOpen(channel Channel, openLoc lnwire.ShortChannelID) error + + // MarkChannelRealScid marks the zero-conf channel's confirmed + // ShortChannelID. + MarkChannelRealScid(channel Channel, + realScid lnwire.ShortChannelID) error + + // MarkChannelScidAliasNegotiated marks that the scid-alias feature + // bit was negotiated during the lifetime of the channel. + MarkChannelScidAliasNegotiated(channel Channel) error +} + // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. type ClosedChannelStore[Channel any] interface { // FetchClosedChannels attempts to fetch all closed channels from the From c60de41d4668497a1969dfd82fa6cb399f51a670 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 17:23:47 -0300 Subject: [PATCH 06/33] chanstate: add open channel status store Add a status facet to the chanstate Store contract for status bit updates and data-loss commit point handling. Implement the facet on ChannelStateDB using the existing persistence code. Update the matching OpenChannel receivers to call through the store methods. The broadcast path still uses a private channeldb helper until its closing-transaction facet is introduced in a later commit. --- channeldb/channel.go | 94 ++++++++++++++++++++++++++++++------------ chanstate/interface.go | 28 +++++++++++++ 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 55ee699dd9..775217d296 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1003,7 +1003,7 @@ func (c *OpenChannel) ApplyChanStatus(status ChannelStatus) error { c.Lock() defer c.Unlock() - return c.putChanStatus(status) + return c.Db.ApplyChannelStatus(c, status) } // ClearChanStatus allows the caller to clear a particular channel status from @@ -1013,7 +1013,7 @@ func (c *OpenChannel) ClearChanStatus(status ChannelStatus) error { c.Lock() defer c.Unlock() - return c.clearChanStatus(status) + return c.Db.ClearChannelStatus(c, status) } // HasChanStatus returns true if the internal bitfield channel status of the @@ -1625,6 +1625,14 @@ func (c *OpenChannel) MarkDataLoss(commitPoint *btcec.PublicKey) error { c.Lock() defer c.Unlock() + return c.Db.MarkChannelDataLoss(c, commitPoint) +} + +// MarkChannelDataLoss marks the channel as local-data-loss and stores the +// commit point needed if the remote force closes. +func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, + commitPoint *btcec.PublicKey) error { + var b bytes.Buffer if err := WriteElement(&b, commitPoint); err != nil { return err @@ -1634,17 +1642,26 @@ func (c *OpenChannel) MarkDataLoss(commitPoint *btcec.PublicKey) error { return chanBucket.Put(dataLossCommitPointKey, b.Bytes()) } - return c.putChanStatus(ChanStatusLocalDataLoss, putCommitPoint) + return c.putChanStatus(channel, ChanStatusLocalDataLoss, putCommitPoint) } // DataLossCommitPoint retrieves the stored commit point set during // MarkDataLoss. If not found ErrNoCommitPoint is returned. func (c *OpenChannel) DataLossCommitPoint() (*btcec.PublicKey, error) { + return c.Db.FetchChannelDataLossCommitPoint(c) +} + +// FetchChannelDataLossCommitPoint retrieves the commit point stored when the +// channel was marked as local-data-loss. +func (c *ChannelStateDB) FetchChannelDataLossCommitPoint( + channel *OpenChannel) (*btcec.PublicKey, error) { + var commitPoint *btcec.PublicKey - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) switch err { case nil: @@ -1681,7 +1698,12 @@ func (c *OpenChannel) MarkBorked() error { c.Lock() defer c.Unlock() - return c.putChanStatus(ChanStatusBorked) + return c.Db.MarkChannelBorked(c) +} + +// MarkChannelBorked marks the channel as irreconcilable. +func (c *ChannelStateDB) MarkChannelBorked(channel *OpenChannel) error { + return c.ApplyChannelStatus(channel, ChanStatusBorked) } // SecondCommitmentPoint returns the second per-commitment-point for use in the @@ -2038,7 +2060,7 @@ func (c *OpenChannel) markBroadcasted(status ChannelStatus, key []byte, status |= ChanStatusRemoteCloseInitiator } - return c.putChanStatus(status, putClosingTx) + return c.Db.putChanStatus(c, status, putClosingTx) } // BroadcastedCommitment retrieves the stored unilateral closing tx set during @@ -2086,30 +2108,41 @@ func (c *OpenChannel) getClosingTx(key []byte) (*wire.MsgTx, error) { return closeTx, 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. -func (c *OpenChannel) putChanStatus(status ChannelStatus, - fs ...func(kvdb.RwBucket) error) error { +// ApplyChannelStatus adds the target status to the channel's persisted status +// bit field. +func (c *ChannelStateDB) ApplyChannelStatus(channel *OpenChannel, + status ChannelStatus) error { + + return c.putChanStatus(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 { - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + if err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - channel, err := fetchOpenChannel(chanBucket, &c.FundingOutpoint) + diskChannel, err := fetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) if err != nil { return err } // Add this status to the existing bitvector found in the DB. - status = channel.chanStatus | status - channel.chanStatus = status + status = diskChannel.chanStatus | status + diskChannel.chanStatus = status - if err := putOpenChannel(chanBucket, channel); err != nil { + if err := putOpenChannel(chanBucket, diskChannel); err != nil { return err } @@ -2130,36 +2163,43 @@ func (c *OpenChannel) putChanStatus(status ChannelStatus, } // Update the in-memory representation to keep it in sync with the DB. - c.chanStatus = status + channel.chanStatus = status return nil } -func (c *OpenChannel) clearChanStatus(status ChannelStatus) error { - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { +// ClearChannelStatus clears the target status from the channel's persisted +// status bit field. +func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, + status ChannelStatus) error { + + if err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - channel, err := fetchOpenChannel(chanBucket, &c.FundingOutpoint) + diskChannel, err := fetchOpenChannel( + chanBucket, &channel.FundingOutpoint, + ) if err != nil { return err } // Unset this bit in the bitvector on disk. - status = channel.chanStatus & ^status - channel.chanStatus = status + status = diskChannel.chanStatus & ^status + diskChannel.chanStatus = status - return putOpenChannel(chanBucket, channel) + return putOpenChannel(chanBucket, diskChannel) }, func() {}); err != nil { return err } // Update the in-memory representation to keep it in sync with the DB. - c.chanStatus = status + channel.chanStatus = status return nil } diff --git a/chanstate/interface.go b/chanstate/interface.go index 6b7a4af6fc..9e60ec10aa 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -28,6 +28,10 @@ type Store[Channel any] interface { // channel records. OpenChannelLifecycleStore[Channel] + // OpenChannelStatusStore owns persisted status flags for open channel + // records. + OpenChannelStatusStore[Channel] + // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. ClosedChannelStore[Channel] @@ -140,6 +144,30 @@ type OpenChannelLifecycleStore[Channel any] interface { MarkChannelScidAliasNegotiated(channel Channel) error } +// OpenChannelStatusStore owns persisted status flags for open channel records. +type OpenChannelStatusStore[Channel any] interface { + // ApplyChannelStatus adds the target status to the channel's + // persisted status bit field. + ApplyChannelStatus(channel Channel, status ChannelStatus) error + + // ClearChannelStatus clears the target status from the channel's + // persisted status bit field. + ClearChannelStatus(channel Channel, status ChannelStatus) error + + // MarkChannelDataLoss marks the channel as local-data-loss and stores + // the commit point needed if the remote force closes. + MarkChannelDataLoss(channel Channel, + commitPoint *btcec.PublicKey) error + + // FetchChannelDataLossCommitPoint retrieves the commit point stored + // when the channel was marked as local-data-loss. + FetchChannelDataLossCommitPoint(channel Channel) ( + *btcec.PublicKey, error) + + // MarkChannelBorked marks the channel as irreconcilable. + MarkChannelBorked(channel Channel) error +} + // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. type ClosedChannelStore[Channel any] interface { // FetchClosedChannels attempts to fetch all closed channels from the From 4dde97e8ae394ff886798a11801d656856723917 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 17:29:15 -0300 Subject: [PATCH 07/33] chanstate: add open channel close stores Add shutdown and close-transaction facets to the chanstate Store contract. These cover persisted shutdown info plus stored unilateral and cooperative closing transactions. Implement the facets on ChannelStateDB with the existing KV code and update OpenChannel receivers to call through the store methods. The backend-specific key selection remains private to channeldb. --- channeldb/channel.go | 103 ++++++++++++++++++++++++++++++----------- chanstate/interface.go | 42 +++++++++++++++++ 2 files changed, 117 insertions(+), 28 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 775217d296..ebce9c36c6 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1917,21 +1917,23 @@ func (c *OpenChannel) MarkShutdownSent(info *ShutdownInfo) error { c.Lock() defer c.Unlock() - return c.storeShutdownInfo(info) + return c.Db.StoreChannelShutdownInfo(c, info) } -// storeShutdownInfo serialises the ShutdownInfo and persists it under the -// shutdownInfoKey. -func (c *OpenChannel) storeShutdownInfo(info *ShutdownInfo) error { +// StoreChannelShutdownInfo persists the ShutdownInfo for the target channel. +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.Db.backend, func(tx kvdb.RwTx) error { + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err @@ -1948,10 +1950,19 @@ func (c *OpenChannel) ShutdownInfo() (fn.Option[ShutdownInfo], error) { c.RLock() defer c.RUnlock() + return c.Db.FetchChannelShutdownInfo(c) +} + +// FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the target +// channel. +func (c *ChannelStateDB) FetchChannelShutdownInfo( + channel *OpenChannel) (fn.Option[ShutdownInfo], error) { + var shutdownInfo *ShutdownInfo - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) switch { case err == nil: @@ -2005,9 +2016,18 @@ func (c *OpenChannel) isBorked(chanBucket kvdb.RBucket) (bool, error) { func (c *OpenChannel) MarkCommitmentBroadcasted(closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { + return c.Db.MarkChannelCommitmentBroadcasted(c, closeTx, closer) +} + +// MarkChannelCommitmentBroadcasted marks the channel as having a commitment +// transaction broadcast. +func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( + channel *OpenChannel, closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error { + return c.markBroadcasted( - ChanStatusCommitBroadcasted, forceCloseTxKey, closeTx, - closer, + channel, ChanStatusCommitBroadcasted, forceCloseTxKey, + closeTx, closer, ) } @@ -2021,21 +2041,29 @@ func (c *OpenChannel) MarkCommitmentBroadcasted(closeTx *wire.MsgTx, func (c *OpenChannel) MarkCoopBroadcasted(closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { + return c.Db.MarkChannelCoopBroadcasted(c, closeTx, closer) +} + +// MarkChannelCoopBroadcasted marks the channel as having a cooperative close +// transaction broadcast. +func (c *ChannelStateDB) MarkChannelCoopBroadcasted(channel *OpenChannel, + closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { + return c.markBroadcasted( - ChanStatusCoopBroadcasted, coopCloseTxKey, closeTx, - closer, + channel, ChanStatusCoopBroadcasted, coopCloseTxKey, + closeTx, closer, ) } -// markBroadcasted is a helper function which modifies the channel status of the -// receiving channel 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 *OpenChannel) markBroadcasted(status ChannelStatus, key []byte, - closeTx *wire.MsgTx, closer lntypes.ChannelParty) error { +// 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 { - c.Lock() - defer c.Unlock() + channel.Lock() + defer channel.Unlock() // If a closing tx is provided, we'll generate a closure to write the // transaction in the appropriate bucket under the given key. @@ -2060,29 +2088,48 @@ func (c *OpenChannel) markBroadcasted(status ChannelStatus, key []byte, status |= ChanStatusRemoteCloseInitiator } - return c.Db.putChanStatus(c, status, putClosingTx) + return c.putChanStatus(channel, status, putClosingTx) } // BroadcastedCommitment retrieves the stored unilateral closing tx set during // MarkCommitmentBroadcasted. If not found ErrNoCloseTx is returned. func (c *OpenChannel) BroadcastedCommitment() (*wire.MsgTx, error) { - return c.getClosingTx(forceCloseTxKey) + return c.Db.FetchChannelBroadcastedCommitment(c) +} + +// FetchChannelBroadcastedCommitment fetches the stored unilateral closing +// transaction. +func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( + channel *OpenChannel) (*wire.MsgTx, error) { + + return c.getClosingTx(channel, forceCloseTxKey) } // BroadcastedCooperative retrieves the stored cooperative closing tx set during // MarkCoopBroadcasted. If not found ErrNoCloseTx is returned. func (c *OpenChannel) BroadcastedCooperative() (*wire.MsgTx, error) { - return c.getClosingTx(coopCloseTxKey) + return c.Db.FetchChannelBroadcastedCooperative(c) } -// getClosingTx is a helper method which returns the stored closing transaction -// for key. The caller should use either the force or coop closing keys. -func (c *OpenChannel) getClosingTx(key []byte) (*wire.MsgTx, error) { +// FetchChannelBroadcastedCooperative fetches the stored cooperative closing +// transaction. +func (c *ChannelStateDB) FetchChannelBroadcastedCooperative( + channel *OpenChannel) (*wire.MsgTx, error) { + + return c.getClosingTx(channel, 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) { + var closeTx *wire.MsgTx - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) switch err { case nil: diff --git a/chanstate/interface.go b/chanstate/interface.go index 9e60ec10aa..a1876d7fa5 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -5,6 +5,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" ) @@ -32,6 +33,12 @@ type Store[Channel any] interface { // records. OpenChannelStatusStore[Channel] + // OpenChannelShutdownStore owns persisted shutdown state. + OpenChannelShutdownStore[Channel] + + // OpenChannelCloseTxStore owns persisted closing transaction state. + OpenChannelCloseTxStore[Channel] + // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. ClosedChannelStore[Channel] @@ -168,6 +175,41 @@ type OpenChannelStatusStore[Channel any] interface { MarkChannelBorked(channel Channel) error } +// OpenChannelShutdownStore owns persisted shutdown state. +type OpenChannelShutdownStore[Channel any] interface { + // StoreChannelShutdownInfo persists the ShutdownInfo for the target + // channel. + StoreChannelShutdownInfo(channel Channel, info *ShutdownInfo) error + + // FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the + // target channel. + FetchChannelShutdownInfo(channel Channel) (fn.Option[ShutdownInfo], + error) +} + +// OpenChannelCloseTxStore owns persisted closing transaction state. +type OpenChannelCloseTxStore[Channel any] interface { + // MarkChannelCommitmentBroadcasted marks the channel as having a + // commitment transaction broadcast. + MarkChannelCommitmentBroadcasted(channel Channel, closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error + + // MarkChannelCoopBroadcasted marks the channel as having a + // cooperative close transaction broadcast. + MarkChannelCoopBroadcasted(channel Channel, closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error + + // FetchChannelBroadcastedCommitment fetches the stored unilateral + // closing transaction. + FetchChannelBroadcastedCommitment(channel Channel) (*wire.MsgTx, + error) + + // FetchChannelBroadcastedCooperative fetches the stored cooperative + // closing transaction. + FetchChannelBroadcastedCooperative(channel Channel) (*wire.MsgTx, + error) +} + // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. type ClosedChannelStore[Channel any] interface { // FetchClosedChannels attempts to fetch all closed channels from the From 67df9801df7682b76e9b1168ce0670ee7c2f5c84 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 17:34:30 -0300 Subject: [PATCH 08/33] chanstate: add pending channel setup store Add pending-channel setup to the chanstate lifecycle store facet. This covers the path that writes a new pending channel and records the funding broadcast height. Move the OpenChannel receiver to call through ChannelStateDB and pass the backend explicitly into the channeldb sync helper. This keeps the link-node persistence detail in channeldb while removing another direct backend reference from OpenChannel. --- channeldb/channel.go | 22 ++++++++++++++++------ channeldb/db.go | 2 +- chanstate/interface.go | 7 +++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index ebce9c36c6..961133496c 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2348,16 +2348,26 @@ func (c *OpenChannel) SyncPending(addr net.Addr, pendingHeight uint32) error { c.Lock() defer c.Unlock() - c.FundingBroadcastHeight = pendingHeight + return c.Db.SyncPendingChannel(c, addr, pendingHeight) +} - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return syncNewChannel(tx, c, []net.Addr{addr}) +// SyncPendingChannel writes a pending channel to the store and records the +// funding broadcast height. +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) }, 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) error { +func syncNewChannel(tx kvdb.RwTx, c *OpenChannel, addrs []net.Addr, + backend kvdb.Backend) error { + // First, sync all the persistent channel state to disk. if err := fullSyncOpenChannel(tx, c); err != nil { return err @@ -2379,8 +2389,8 @@ func syncNewChannel(tx kvdb.RwTx, c *OpenChannel, addrs []net.Addr) error { // for this channel. The LinkNode metadata contains reachability, // up-time, and service bits related information. linkNode := NewLinkNode( - &LinkNodeDB{backend: c.Db.backend}, - wire.MainNet, c.IdentityPub, addrs..., + &LinkNodeDB{backend: backend}, wire.MainNet, c.IdentityPub, + addrs..., ) // TODO(roasbeef): do away with link node all together? diff --git a/channeldb/db.go b/channeldb/db.go index b76c0d61f0..514264dba1 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -1702,7 +1702,7 @@ func (c *ChannelStateDB) RestoreChannelShells(channelShells ...*ChannelShell) er // is idempotent, we'll continue to the next step. channel.Db = c err := syncNewChannel( - tx, channel, channelShell.NodeAddrs, + tx, channel, channelShell.NodeAddrs, c.backend, ) if err != nil { return err diff --git a/chanstate/interface.go b/chanstate/interface.go index a1876d7fa5..d148729b75 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -1,6 +1,8 @@ package chanstate import ( + "net" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" @@ -122,6 +124,11 @@ type HistoricalChannelStore[Channel any] interface { // OpenChannelLifecycleStore owns persisted lifecycle state for open channel // records. type OpenChannelLifecycleStore[Channel any] interface { + // SyncPendingChannel writes a pending channel to the store and records + // the funding broadcast height. + SyncPendingChannel(channel Channel, addr net.Addr, + pendingHeight uint32) error + // RefreshChannel updates the in-memory channel state using the latest // state observed on disk. RefreshChannel(channel Channel) error From c68a8ce6a5c924b6e3e0bf6b717472545963cff5 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 17:54:30 -0300 Subject: [PATCH 09/33] chanstate: move commitment value types Move ChannelCommitment and HTLC into chanstate so upcoming store facets can name commitment state without importing channeldb. Leave the KV serialization helpers in channeldb and keep aliases for existing call sites. This preserves the current disk format and keeps backend-specific persistence code out of chanstate for now. --- channeldb/channel.go | 251 +++++----------------------------------- chanstate/commitment.go | 217 ++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 225 deletions(-) create mode 100644 chanstate/commitment.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 961133496c..9bd73d2cb6 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -227,6 +227,15 @@ const ( indexStatusType tlv.Type = 0 ) +type ( + // ChannelCommitment is a snapshot of the commitment state at a + // particular point in the commitment chain. + ChannelCommitment = cstate.ChannelCommitment + + // HTLC is the on-disk representation of a hash time-locked contract. + HTLC = cstate.HTLC +) + // 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 @@ -518,97 +527,15 @@ func (c *commitTlvData) decode(r io.Reader) error { return nil } -// ChannelCommitment is a snapshot of the commitment state at a particular -// point in the commitment chain. With each state transition, a snapshot of the -// current state along with all non-settled HTLCs are recorded. These snapshots -// detail the state of the _remote_ party's commitment at a particular state -// number. For ourselves (the local node) we ONLY store our most recent -// (unrevoked) state for safety purposes. -type ChannelCommitment struct { - // CommitHeight is the update number that this ChannelDelta represents - // the total number of commitment updates to this point. This can be - // viewed as sort of a "commitment height" as this number is - // monotonically increasing. - CommitHeight uint64 - - // LocalLogIndex is the cumulative log index index of the local node at - // this point in the commitment chain. This value will be incremented - // for each _update_ added to the local update log. - LocalLogIndex uint64 - - // LocalHtlcIndex is the current local running HTLC index. This value - // will be incremented for each outgoing HTLC the local node offers. - LocalHtlcIndex uint64 - - // RemoteLogIndex is the cumulative log index index of the remote node - // at this point in the commitment chain. This value will be - // incremented for each _update_ added to the remote update log. - RemoteLogIndex uint64 - - // RemoteHtlcIndex is the current remote running HTLC index. This value - // will be incremented for each outgoing HTLC the remote node offers. - RemoteHtlcIndex uint64 - - // LocalBalance is the current available settled balance within the - // channel directly spendable by us. - // - // NOTE: This is the balance *after* subtracting any commitment fee, - // AND anchor output values. - LocalBalance lnwire.MilliSatoshi - - // RemoteBalance is the current available settled balance within the - // channel directly spendable by the remote node. - // - // NOTE: This is the balance *after* subtracting any commitment fee, - // AND anchor output values. - RemoteBalance lnwire.MilliSatoshi - - // CommitFee is the amount calculated to be paid in fees for the - // current set of commitment transactions. The fee amount is persisted - // with the channel in order to allow the fee amount to be removed and - // recalculated with each channel state update, including updates that - // happen after a system restart. - CommitFee btcutil.Amount - - // FeePerKw is the min satoshis/kilo-weight that should be paid within - // the commitment transaction for the entire duration of the channel's - // lifetime. This field may be updated during normal operation of the - // channel as on-chain conditions change. - // - // TODO(halseth): make this SatPerKWeight. Cannot be done atm because - // this will cause the import cycle lnwallet<->channeldb. Fee - // estimation stuff should be in its own package. - FeePerKw btcutil.Amount - - // CommitTx is the latest version of the commitment state, broadcast - // able by us. - CommitTx *wire.MsgTx - - // CustomBlob is an optional blob that can be used to store information - // specific to a custom channel type. This may track some custom - // specific state for this given commitment. - CustomBlob fn.Option[tlv.Blob] - - // CommitSig is one half of the signature required to fully complete - // the script for the commitment transaction above. This is the - // signature signed by the remote party for our version of the - // commitment transactions. - CommitSig []byte - - // Htlcs is the set of HTLC's that are pending at this particular - // commitment height. - Htlcs []HTLC -} - -// amendTlvData updates the channel with the given auxiliary TLV data. -func (c *ChannelCommitment) amendTlvData(auxData commitTlvData) { +// 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) }) } -// extractTlvData creates a new commitTlvData from the given commitment. -func (c *ChannelCommitment) extractTlvData() commitTlvData { +// extractCommitTlvData creates a new commitTlvData from the given commitment. +func extractCommitTlvData(c *ChannelCommitment) commitTlvData { var auxData commitTlvData c.CustomBlob.WhenSome(func(blob tlv.Blob) { @@ -620,33 +547,6 @@ func (c *ChannelCommitment) extractTlvData() commitTlvData { return auxData } -// copy returns a deep copy of the channel commitment. -func (c *ChannelCommitment) copy() ChannelCommitment { - c2 := *c - if c.CommitTx != nil { - c2.CommitTx = c.CommitTx.Copy() - } - if len(c.CommitSig) > 0 { - c2.CommitSig = make([]byte, len(c.CommitSig)) - copy(c2.CommitSig, c.CommitSig) - } - - c.CustomBlob.WhenSome(func(blob tlv.Blob) { - blobCopy := make([]byte, len(blob)) - copy(blobCopy, blob) - c2.CustomBlob = fn.Some(blobCopy) - }) - - if len(c.Htlcs) > 0 { - c2.Htlcs = make([]HTLC, len(c.Htlcs)) - for i, h := range c.Htlcs { - c2.Htlcs[i] = h.Copy() - } - } - - return c2 -} - // 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 @@ -2642,89 +2542,7 @@ func (c *OpenChannel) ActiveHtlcs() []HTLC { return activeHtlcs } -// HTLC is the on-disk representation of a hash time-locked contract. HTLCs are -// contained within ChannelDeltas which encode the current state of the -// commitment between state updates. -// -// TODO(roasbeef): save space by using smaller ints at tail end? -type HTLC struct { - // TODO(yy): can embed an HTLCEntry here. - - // Signature is the signature for the second level covenant transaction - // for this HTLC. The second level transaction is a timeout tx in the - // case that this is an outgoing HTLC, and a success tx in the case - // that this is an incoming HTLC. - // - // TODO(roasbeef): make [64]byte instead? - Signature []byte - - // RHash is the payment hash of the HTLC. - RHash [32]byte - - // Amt is the amount of milli-satoshis this HTLC escrows. - Amt lnwire.MilliSatoshi - - // RefundTimeout is the absolute timeout on the HTLC that the sender - // must wait before reclaiming the funds in limbo. - RefundTimeout uint32 - - // OutputIndex is the output index for this particular HTLC output - // within the commitment transaction. - OutputIndex int32 - - // Incoming denotes whether we're the receiver or the sender of this - // HTLC. - Incoming bool - - // OnionBlob is an opaque blob which is used to complete multi-hop - // routing. - OnionBlob [lnwire.OnionPacketSize]byte - - // HtlcIndex is the HTLC counter index of this active, outstanding - // HTLC. This differs from the LogIndex, as the HtlcIndex is only - // incremented for each offered HTLC, while they LogIndex is - // incremented for each update (includes settle+fail). - HtlcIndex uint64 - - // LogIndex is the cumulative log index of this HTLC. This differs - // from the HtlcIndex as this will be incremented for each new log - // update added. - LogIndex uint64 - - // ExtraData contains any additional information that was transmitted - // with the HTLC via TLVs. This data *must* already be encoded as a - // TLV stream, and may be empty. The length of this data is naturally - // limited by the space available to TLVs in update_add_htlc: - // = 65535 bytes (bolt 8 maximum message size): - // - 2 bytes (bolt 1 message_type) - // - 32 bytes (channel_id) - // - 8 bytes (id) - // - 8 bytes (amount_msat) - // - 32 bytes (payment_hash) - // - 4 bytes (cltv_expiry) - // - 1366 bytes (onion_routing_packet) - // = 64083 bytes maximum possible TLV stream - // - // Note that this extra data is stored inline with the OnionBlob for - // legacy reasons, see serialization/deserialization functions for - // detail. - ExtraData lnwire.ExtraOpaqueData - - // BlindingPoint is an optional blinding point included with the HTLC. - // - // Note: this field is not a part of on-disk representation of the - // HTLC. It is stored in the ExtraData field, which is used to store - // a TLV stream of additional information associated with the HTLC. - BlindingPoint lnwire.BlindingPointRecord - - // CustomRecords is a set of custom TLV records that are associated with - // this HTLC. These records are used to store additional information - // about the HTLC that is not part of the standard HTLC fields. This - // field is encoded within the ExtraData field. - CustomRecords lnwire.CustomRecords -} - -// serializeExtraData encodes a TLV stream of extra data to be stored with a +// 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 @@ -2732,7 +2550,7 @@ type HTLC struct { // // This function MUST be called to persist all HTLC values when they are // serialized. -func (h *HTLC) serializeExtraData() error { +func serializeHtlcExtraData(h *HTLC) error { var records []tlv.RecordProducer h.BlindingPoint.WhenSome(func(b tlv.RecordT[lnwire.BlindingPointTlvType, *btcec.PublicKey]) { @@ -2748,12 +2566,12 @@ func (h *HTLC) serializeExtraData() error { return h.ExtraData.PackRecords(records...) } -// deserializeExtraData extracts TLVs from the extra data persisted for the -// htlc and populates values in the struct accordingly. +// 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 (h *HTLC) deserializeExtraData() error { +func deserializeHtlcExtraData(h *HTLC) error { if len(h.ExtraData) == 0 { return nil } @@ -2805,7 +2623,7 @@ func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error { for _, htlc := range htlcs { // Populate TLV stream for any additional fields contained // in the TLV. - if err := htlc.serializeExtraData(); err != nil { + if err := serializeHtlcExtraData(&htlc); err != nil { return err } @@ -2897,7 +2715,7 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { // Finally, deserialize any TLVs contained in that extra data // if they are present. - if err := htlcs[i].deserializeExtraData(); err != nil { + if err := deserializeHtlcExtraData(&htlcs[i]); err != nil { return nil, err } } @@ -2905,23 +2723,6 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { return htlcs, nil } -// Copy returns a full copy of the target HTLC. -func (h *HTLC) Copy() HTLC { - clone := HTLC{ - Incoming: h.Incoming, - Amt: h.Amt, - RefundTimeout: h.RefundTimeout, - OutputIndex: h.OutputIndex, - } - copy(clone.Signature[:], h.Signature) - copy(clone.RHash[:], h.RHash[:]) - copy(clone.ExtraData, h.ExtraData) - clone.BlindingPoint = h.BlindingPoint - clone.CustomRecords = h.CustomRecords.Copy() - - return clone -} - // LogUpdate represents a pending update to the remote commitment chain. The // log update may be an add, fail, or settle entry. We maintain this data in // order to be able to properly retransmit our proposed state if necessary. @@ -3082,7 +2883,7 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl // 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 := diff.Commitment.extractTlvData() + auxData := extractCommitTlvData(&diff.Commitment) if err := auxData.encode(w); err != nil { return fmt.Errorf("unable to write aux data: %w", err) } @@ -3156,7 +2957,7 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) { return nil, fmt.Errorf("unable to decode aux data: %w", err) } - d.Commitment.amendTlvData(auxData) + amendCommitTlvData(&d.Commitment, auxData) return &d, nil } @@ -4174,8 +3975,8 @@ func (c *OpenChannel) Copy() *OpenChannel { InitialRemoteBalance: c.InitialRemoteBalance, LocalChanCfg: c.LocalChanCfg, RemoteChanCfg: c.RemoteChanCfg, - LocalCommitment: c.LocalCommitment.copy(), - RemoteCommitment: c.RemoteCommitment.copy(), + LocalCommitment: c.LocalCommitment.Copy(), + RemoteCommitment: c.RemoteCommitment.Copy(), RemoteCurrentRevocation: c.RemoteCurrentRevocation, RemoteNextRevocation: c.RemoteNextRevocation, RevocationProducer: c.RevocationProducer, @@ -4631,7 +4432,7 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, } // Before we write to disk, we'll also write our aux data as well. - auxData := c.extractTlvData() + auxData := extractCommitTlvData(c) if err := auxData.encode(&b); err != nil { return fmt.Errorf("unable to write aux data: %w", err) } @@ -4811,7 +4612,7 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, "chan aux data: %w", err) } - chanCommit.amendTlvData(auxData) + amendCommitTlvData(&chanCommit, auxData) return chanCommit, nil } diff --git a/chanstate/commitment.go b/chanstate/commitment.go new file mode 100644 index 0000000000..aef1121905 --- /dev/null +++ b/chanstate/commitment.go @@ -0,0 +1,217 @@ +package chanstate + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// ChannelCommitment is a snapshot of the commitment state at a particular +// point in the commitment chain. With each state transition, a snapshot of the +// current state along with all non-settled HTLCs are recorded. These snapshots +// detail the state of the _remote_ party's commitment at a particular state +// number. For ourselves (the local node) we ONLY store our most recent +// (unrevoked) state for safety purposes. +type ChannelCommitment struct { + // CommitHeight is the update number that this ChannelDelta represents + // the total number of commitment updates to this point. This can be + // viewed as sort of a "commitment height" as this number is + // monotonically increasing. + CommitHeight uint64 + + // LocalLogIndex is the cumulative log index of the local node at this + // point in the commitment chain. This value will be incremented for + // each _update_ added to the local update log. + LocalLogIndex uint64 + + // LocalHtlcIndex is the current local running HTLC index. This value + // will be incremented for each outgoing HTLC the local node offers. + LocalHtlcIndex uint64 + + // RemoteLogIndex is the cumulative log index of the remote node at + // this point in the commitment chain. This value will be incremented + // for each _update_ added to the remote update log. + RemoteLogIndex uint64 + + // RemoteHtlcIndex is the current remote running HTLC index. This value + // will be incremented for each outgoing HTLC the remote node offers. + RemoteHtlcIndex uint64 + + // LocalBalance is the current available settled balance within the + // channel directly spendable by us. + // + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. + LocalBalance lnwire.MilliSatoshi + + // RemoteBalance is the current available settled balance within the + // channel directly spendable by the remote node. + // + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. + RemoteBalance lnwire.MilliSatoshi + + // CommitFee is the amount calculated to be paid in fees for the + // current set of commitment transactions. The fee amount is persisted + // with the channel in order to allow the fee amount to be removed and + // recalculated with each channel state update, including updates that + // happen after a system restart. + CommitFee btcutil.Amount + + // FeePerKw is the min satoshis/kilo-weight that should be paid within + // the commitment transaction for the entire duration of the channel's + // lifetime. This field may be updated during normal operation of the + // channel as on-chain conditions change. + // + // TODO(halseth): make this SatPerKWeight. Cannot be done atm because + // this will cause the import cycle lnwallet<->channeldb. Fee + // estimation stuff should be in its own package. + FeePerKw btcutil.Amount + + // CommitTx is the latest version of the commitment state, broadcast + // able by us. + CommitTx *wire.MsgTx + + // CustomBlob is an optional blob that can be used to store information + // specific to a custom channel type. This may track some custom + // specific state for this given commitment. + CustomBlob fn.Option[tlv.Blob] + + // CommitSig is one half of the signature required to fully complete + // the script for the commitment transaction above. This is the + // signature signed by the remote party for our version of the + // commitment transactions. + CommitSig []byte + + // Htlcs is the set of HTLC's that are pending at this particular + // commitment height. + Htlcs []HTLC +} + +// Copy returns a deep copy of the channel commitment. +func (c *ChannelCommitment) Copy() ChannelCommitment { + c2 := *c + if c.CommitTx != nil { + c2.CommitTx = c.CommitTx.Copy() + } + if len(c.CommitSig) > 0 { + c2.CommitSig = make([]byte, len(c.CommitSig)) + copy(c2.CommitSig, c.CommitSig) + } + + c.CustomBlob.WhenSome(func(blob tlv.Blob) { + blobCopy := make([]byte, len(blob)) + copy(blobCopy, blob) + c2.CustomBlob = fn.Some(blobCopy) + }) + + if len(c.Htlcs) > 0 { + c2.Htlcs = make([]HTLC, len(c.Htlcs)) + for i, h := range c.Htlcs { + c2.Htlcs[i] = h.Copy() + } + } + + return c2 +} + +// HTLC is the on-disk representation of a hash time-locked contract. HTLCs are +// contained within ChannelDeltas which encode the current state of the +// commitment between state updates. +// +// TODO(roasbeef): save space by using smaller ints at tail end? +type HTLC struct { + // TODO(yy): can embed an HTLCEntry here. + + // Signature is the signature for the second level covenant transaction + // for this HTLC. The second level transaction is a timeout tx in the + // case that this is an outgoing HTLC, and a success tx in the case + // that this is an incoming HTLC. + // + // TODO(roasbeef): make [64]byte instead? + Signature []byte + + // RHash is the payment hash of the HTLC. + RHash [32]byte + + // Amt is the amount of milli-satoshis this HTLC escrows. + Amt lnwire.MilliSatoshi + + // RefundTimeout is the absolute timeout on the HTLC that the sender + // must wait before reclaiming the funds in limbo. + RefundTimeout uint32 + + // OutputIndex is the output index for this particular HTLC output + // within the commitment transaction. + OutputIndex int32 + + // Incoming denotes whether we're the receiver or the sender of this + // HTLC. + Incoming bool + + // OnionBlob is an opaque blob which is used to complete multi-hop + // routing. + OnionBlob [lnwire.OnionPacketSize]byte + + // HtlcIndex is the HTLC counter index of this active, outstanding + // HTLC. This differs from the LogIndex, as the HtlcIndex is only + // incremented for each offered HTLC, while they LogIndex is + // incremented for each update (includes settle+fail). + HtlcIndex uint64 + + // LogIndex is the cumulative log index of this HTLC. This differs + // from the HtlcIndex as this will be incremented for each new log + // update added. + LogIndex uint64 + + // ExtraData contains any additional information that was transmitted + // with the HTLC via TLVs. This data *must* already be encoded as a + // TLV stream, and may be empty. The length of this data is naturally + // limited by the space available to TLVs in update_add_htlc: + // = 65535 bytes (bolt 8 maximum message size): + // - 2 bytes (bolt 1 message_type) + // - 32 bytes (channel_id) + // - 8 bytes (id) + // - 8 bytes (amount_msat) + // - 32 bytes (payment_hash) + // - 4 bytes (cltv_expiry) + // - 1366 bytes (onion_routing_packet) + // = 64083 bytes maximum possible TLV stream + // + // Note that this extra data is stored inline with the OnionBlob for + // legacy reasons, see serialization/deserialization functions for + // detail. + ExtraData lnwire.ExtraOpaqueData + + // BlindingPoint is an optional blinding point included with the HTLC. + // + // Note: this field is not a part of on-disk representation of the + // HTLC. It is stored in the ExtraData field, which is used to store + // a TLV stream of additional information associated with the HTLC. + BlindingPoint lnwire.BlindingPointRecord + + // CustomRecords is a set of custom TLV records that are associated with + // this HTLC. These records are used to store additional information + // about the HTLC that is not part of the standard HTLC fields. This + // field is encoded within the ExtraData field. + CustomRecords lnwire.CustomRecords +} + +// Copy returns a full copy of the target HTLC. +func (h *HTLC) Copy() HTLC { + clone := HTLC{ + Incoming: h.Incoming, + Amt: h.Amt, + RefundTimeout: h.RefundTimeout, + OutputIndex: h.OutputIndex, + } + copy(clone.Signature, h.Signature) + copy(clone.RHash[:], h.RHash[:]) + copy(clone.ExtraData, h.ExtraData) + clone.BlindingPoint = h.BlindingPoint + clone.CustomRecords = h.CustomRecords.Copy() + + return clone +} From 8bd8ecda16ca91548baab453beb7fff363fb7cbb Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:04:49 -0300 Subject: [PATCH 10/33] chanstate: move log update type Move LogUpdate into chanstate so commitment store interfaces can refer to pending update state without importing channeldb. Keep the log-update serialization helpers in channeldb. Those helpers remain part of the existing KV disk format and can move with the KV backend implementation later. --- channeldb/channel.go | 18 ++++-------------- chanstate/commitment.go | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 9bd73d2cb6..f94cd4829e 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -234,6 +234,10 @@ type ( // HTLC is the on-disk representation of a hash time-locked contract. HTLC = cstate.HTLC + + // LogUpdate represents a pending update to the remote commitment + // chain. + LogUpdate = cstate.LogUpdate ) // openChannelTlvData houses the new data fields that are stored for each @@ -2723,20 +2727,6 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { return htlcs, nil } -// LogUpdate represents a pending update to the remote commitment chain. The -// log update may be an add, fail, or settle entry. We maintain this data in -// order to be able to properly retransmit our proposed state if necessary. -type LogUpdate struct { - // LogIndex is the log index of this proposed commitment update entry. - LogIndex uint64 - - // UpdateMsg is the update message that was included within our - // local update log. The LogIndex value denotes the log index of this - // update which will be used when restoring our local update log if - // we're left with a dangling update on restart. - UpdateMsg lnwire.Message -} - // 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) diff --git a/chanstate/commitment.go b/chanstate/commitment.go index aef1121905..b672c8e9ac 100644 --- a/chanstate/commitment.go +++ b/chanstate/commitment.go @@ -215,3 +215,17 @@ func (h *HTLC) Copy() HTLC { return clone } + +// LogUpdate represents a pending update to the remote commitment chain. The +// log update may be an add, fail, or settle entry. We maintain this data in +// order to be able to properly retransmit our proposed state if necessary. +type LogUpdate struct { + // LogIndex is the log index of this proposed commitment update entry. + LogIndex uint64 + + // UpdateMsg is the update message that was included within our + // local update log. The LogIndex value denotes the log index of this + // update which will be used when restoring our local update log if + // we're left with a dangling update on restart. + UpdateMsg lnwire.Message +} From 1ff4a4c8a02aaf233755c34933d17573d040659b Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:23:38 -0300 Subject: [PATCH 11/33] chanstate: add commitment store facet Add a commitment-focused store facet for updating local channel commitment state. This lets OpenChannel call through the chanstate store contract instead of reaching directly into the KV backend. Keep the existing KV transaction body on ChannelStateDB for now. The receiver still owns locking and in-memory state updates while the store method owns persistence. --- channeldb/channel.go | 32 ++++++++++++++++++++++++-------- chanstate/interface.go | 17 +++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index f94cd4829e..7dc4d4d2dc 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2327,11 +2327,29 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, return nil, ErrNoRestoredChannelMutation } + finalHtlcs, err := c.Db.UpdateChannelCommitment( + c, newCommitment, unsignedAckedUpdates, + ) + if err != nil { + return nil, err + } + + c.LocalCommitment = *newCommitment + + return finalHtlcs, nil +} + +// UpdateChannelCommitment updates the local commitment state. +func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, + newCommitment *ChannelCommitment, + unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) { + var finalHtlcs = make(map[uint64]bool) - err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err @@ -2339,7 +2357,7 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, // If the channel is marked as borked, then for safety reasons, // we shouldn't attempt any further updates. - isBorked, err := c.isBorked(chanBucket) + isBorked, err := channel.isBorked(chanBucket) if err != nil { return err } @@ -2347,7 +2365,7 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, return ErrChanBorked } - if err = putChanInfo(chanBucket, c); err != nil { + if err = putChanInfo(chanBucket, channel); err != nil { return fmt.Errorf("unable to store chan info: %w", err) } @@ -2402,9 +2420,9 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, // Get the bucket where settled htlcs are recorded if the user // opted in to storing this information. var finalHtlcsBucket kvdb.RwBucket - if c.Db.parent.storeFinalHtlcResolutions { + if c.parent.storeFinalHtlcResolutions { bucket, err := fetchFinalHtlcsBucketRw( - tx, c.ShortChannelID, + tx, channel.ShortChannelID, ) if err != nil { return err @@ -2453,8 +2471,6 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, return nil, err } - c.LocalCommitment = *newCommitment - return finalHtlcs, nil } diff --git a/chanstate/interface.go b/chanstate/interface.go index d148729b75..6a2f4d87b2 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -41,6 +41,10 @@ type Store[Channel any] interface { // OpenChannelCloseTxStore owns persisted closing transaction state. OpenChannelCloseTxStore[Channel] + // OpenChannelCommitmentStore owns persisted commitment state for open + // channel records. + OpenChannelCommitmentStore[Channel] + // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. ClosedChannelStore[Channel] @@ -217,6 +221,19 @@ type OpenChannelCloseTxStore[Channel any] interface { error) } +// OpenChannelCommitmentStore owns persisted commitment state for open channel +// records. +type OpenChannelCommitmentStore[Channel any] interface { + // UpdateChannelCommitment updates the local commitment state. It + // locks in pending local updates received from the remote party and + // persists remote log updates that have been acked, but not signed + // for yet. The returned map contains all HTLC resolutions locked into + // this commitment, keyed by HTLC index. + UpdateChannelCommitment(channel Channel, + newCommitment *ChannelCommitment, + unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) +} + // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. type ClosedChannelStore[Channel any] interface { // FetchClosedChannels attempts to fetch all closed channels from the From e844d230acf76b54ce9b780f9c7699352673861c Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:28:18 -0300 Subject: [PATCH 12/33] chanstate: move commitment diff types Move CommitDiff and its forwarding reference types into chanstate. This lets the next commitment store facet name pending remote commitment state without importing channeldb. Keep forwarding package persistence and commit-diff serialization in channeldb for now. The aliases preserve existing call sites while the KV backend code remains in place. --- channeldb/channel.go | 59 +++------------------------------ channeldb/forwarding_package.go | 59 ++++++--------------------------- chanstate/commitment.go | 56 +++++++++++++++++++++++++++++++ chanstate/forwarding.go | 57 +++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 104 deletions(-) create mode 100644 chanstate/forwarding.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 7dc4d4d2dc..d0cccfaf3c 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -238,6 +238,10 @@ type ( // LogUpdate represents a pending update to the remote commitment // chain. LogUpdate = cstate.LogUpdate + + // CommitDiff represents the delta needed to apply the state + // transition between two subsequent commitment states. + CommitDiff = cstate.CommitDiff ) // openChannelTlvData houses the new data fields that are stored for each @@ -2758,61 +2762,6 @@ func deserializeLogUpdate(r io.Reader) (*LogUpdate, error) { return l, nil } -// CommitDiff represents the delta needed to apply the state transition between -// two subsequent commitment states. Given state N and state N+1, one is able -// to apply the set of messages contained within the CommitDiff to N to arrive -// at state N+1. Each time a new commitment is extended, we'll write a new -// commitment (along with the full commitment state) to disk so we can -// re-transmit the state in the case of a connection loss or message drop. -type CommitDiff struct { - // ChannelCommitment is the full commitment state that one would arrive - // at by applying the set of messages contained in the UpdateDiff to - // the prior accepted commitment. - Commitment ChannelCommitment - - // LogUpdates is the set of messages sent prior to the commitment state - // transition in question. Upon reconnection, if we detect that they - // don't have the commitment, then we re-send this along with the - // proper signature. - LogUpdates []LogUpdate - - // CommitSig is the exact CommitSig message that should be sent after - // the set of LogUpdates above has been retransmitted. The signatures - // within this message should properly cover the new commitment state - // and also the HTLC's within the new commitment state. - CommitSig *lnwire.CommitSig - - // OpenedCircuitKeys is a set of unique identifiers for any downstream - // Add packets included in this commitment txn. After a restart, this - // set of htlcs is acked from the link's incoming mailbox to ensure - // there isn't an attempt to re-add them to this commitment txn. - OpenedCircuitKeys []models.CircuitKey - - // ClosedCircuitKeys records the unique identifiers for any settle/fail - // packets that were resolved by this commitment txn. After a restart, - // this is used to ensure those circuits are removed from the circuit - // map, and the downstream packets in the link's mailbox are removed. - ClosedCircuitKeys []models.CircuitKey - - // AddAcks specifies the locations (commit height, pkg index) of any - // Adds that were failed/settled in this commit diff. This will ack - // entries in *this* channel's forwarding packages. - // - // NOTE: This value is not serialized, it is used to atomically mark the - // resolution of adds, such that they will not be reprocessed after a - // restart. - AddAcks []AddRef - - // SettleFailAcks specifies the locations (chan id, commit height, pkg - // index) of any Settles or Fails that were locked into this commit - // diff, and originate from *another* channel, i.e. the outgoing link. - // - // NOTE: This value is not serialized, it is used to atomically acks - // settles and fails from the forwarding packages of other channels, - // such that they will not be reforwarded internally after a restart. - SettleFailAcks []SettleFailRef -} - // serializeLogUpdates serializes provided list of updates to a stream. func serializeLogUpdates(w io.Writer, logUpdates []LogUpdate) error { numUpdates := uint16(len(logUpdates)) diff --git a/channeldb/forwarding_package.go b/channeldb/forwarding_package.go index c393a53b37..8ba2c955d9 100644 --- a/channeldb/forwarding_package.go +++ b/channeldb/forwarding_package.go @@ -7,10 +7,20 @@ import ( "fmt" "io" + cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" ) +type ( + // AddRef is used to identify a particular Add in a FwdPkg. + AddRef = cstate.AddRef + + // SettleFailRef is used to locate a Settle/Fail in another channel's + // FwdPkg. + SettleFailRef = cstate.SettleFailRef +) + // ErrCorruptedFwdPkg signals that the on-disk structure of the forwarding // package has potentially been mangled. var ErrCorruptedFwdPkg = errors.New("fwding package db has been corrupted") @@ -327,55 +337,6 @@ func (f *FwdPkg) String() string { f, f.Source, f.Height, len(f.Adds), len(f.SettleFails)) } -// AddRef is used to identify a particular Add in a FwdPkg. The short channel ID -// is assumed to be that of the packager. -type AddRef struct { - // Height is the remote commitment height that locked in the Add. - Height uint64 - - // Index is the index of the Add within the fwd pkg's Adds. - // - // NOTE: This index is static over the lifetime of a forwarding package. - 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. -type SettleFailRef struct { - // Source identifies the outgoing link that locked in the settle or - // fail. This is then used by the *incoming* link to find the settle - // fail in another link's forwarding packages. - Source lnwire.ShortChannelID - - // Height is the remote commitment height that locked in this - // Settle/Fail. - Height uint64 - - // Index is the index of the Add with the fwd pkg's SettleFails. - // - // NOTE: This index is static over the lifetime of a forwarding package. - Index uint16 -} - // SettleFailAcker is a generic interface providing the ability to acknowledge // settle/fail HTLCs stored in forwarding packages. type SettleFailAcker interface { diff --git a/chanstate/commitment.go b/chanstate/commitment.go index b672c8e9ac..c653b3cd0d 100644 --- a/chanstate/commitment.go +++ b/chanstate/commitment.go @@ -4,6 +4,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" ) @@ -229,3 +230,58 @@ type LogUpdate struct { // we're left with a dangling update on restart. UpdateMsg lnwire.Message } + +// CommitDiff represents the delta needed to apply the state transition between +// two subsequent commitment states. Given state N and state N+1, one is able +// to apply the set of messages contained within the CommitDiff to N to arrive +// at state N+1. Each time a new commitment is extended, we'll write a new +// commitment (along with the full commitment state) to disk so we can +// re-transmit the state in the case of a connection loss or message drop. +type CommitDiff struct { + // ChannelCommitment is the full commitment state that one would arrive + // at by applying the set of messages contained in the UpdateDiff to + // the prior accepted commitment. + Commitment ChannelCommitment + + // LogUpdates is the set of messages sent prior to the commitment state + // transition in question. Upon reconnection, if we detect that they + // don't have the commitment, then we re-send this along with the + // proper signature. + LogUpdates []LogUpdate + + // CommitSig is the exact CommitSig message that should be sent after + // the set of LogUpdates above has been retransmitted. The signatures + // within this message should properly cover the new commitment state + // and also the HTLC's within the new commitment state. + CommitSig *lnwire.CommitSig + + // OpenedCircuitKeys is a set of unique identifiers for any downstream + // Add packets included in this commitment txn. After a restart, this + // set of htlcs is acked from the link's incoming mailbox to ensure + // there isn't an attempt to re-add them to this commitment txn. + OpenedCircuitKeys []models.CircuitKey + + // ClosedCircuitKeys records the unique identifiers for any settle/fail + // packets that were resolved by this commitment txn. After a restart, + // this is used to ensure those circuits are removed from the circuit + // map, and the downstream packets in the link's mailbox are removed. + ClosedCircuitKeys []models.CircuitKey + + // AddAcks specifies the locations (commit height, pkg index) of any + // Adds that were failed/settled in this commit diff. This will ack + // entries in *this* channel's forwarding packages. + // + // NOTE: This value is not serialized, it is used to atomically mark the + // resolution of adds, such that they will not be reprocessed after a + // restart. + AddAcks []AddRef + + // SettleFailAcks specifies the locations (chan id, commit height, pkg + // index) of any Settles or Fails that were locked into this commit + // diff, and originate from *another* channel, i.e. the outgoing link. + // + // NOTE: This value is not serialized, it is used to atomically acks + // settles and fails from the forwarding packages of other channels, + // such that they will not be reforwarded internally after a restart. + SettleFailAcks []SettleFailRef +} diff --git a/chanstate/forwarding.go b/chanstate/forwarding.go new file mode 100644 index 0000000000..9cc830ff12 --- /dev/null +++ b/chanstate/forwarding.go @@ -0,0 +1,57 @@ +package chanstate + +import ( + "encoding/binary" + "io" + + "github.com/lightningnetwork/lnd/lnwire" +) + +// AddRef is used to identify a particular Add in a FwdPkg. The short channel ID +// is assumed to be that of the packager. +type AddRef struct { + // Height is the remote commitment height that locked in the Add. + Height uint64 + + // Index is the index of the Add within the fwd pkg's Adds. + // + // NOTE: This index is static over the lifetime of a forwarding package. + 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. +type SettleFailRef struct { + // Source identifies the outgoing link that locked in the settle or + // fail. This is then used by the *incoming* link to find the settle + // fail in another link's forwarding packages. + Source lnwire.ShortChannelID + + // Height is the remote commitment height that locked in this + // Settle/Fail. + Height uint64 + + // Index is the index of the Add with the fwd pkg's SettleFails. + // + // NOTE: This index is static over the lifetime of a forwarding package. + Index uint16 +} From ac68cb49e497532c8814ca75f6d81a287c10ada1 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:32:59 -0300 Subject: [PATCH 13/33] chanstate: add remote commit chain store Add the remote commitment-chain append method to the chanstate commitment store facet. Move the existing KV transaction body onto ChannelStateDB and have the OpenChannel receiver call through the store. This removes another direct backend dependency from OpenChannel while keeping KV persistence code in channeldb. --- channeldb/channel.go | 21 ++++++++++++++++----- chanstate/interface.go | 5 +++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index d0cccfaf3c..f96894c609 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2934,11 +2934,20 @@ func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error { return ErrNoRestoredChannelMutation } - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + return c.Db.AppendRemoteCommitChain(c, diff) +} + +// AppendRemoteCommitChain appends a new CommitDiff to the remote party's +// commitment chain. +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, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err @@ -2946,7 +2955,7 @@ func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error { // If the channel is marked as borked, then for safety reasons, // we shouldn't attempt any further updates. - isBorked, err := c.isBorked(chanBucket) + isBorked, err := channel.isBorked(chanBucket) if err != nil { return err } @@ -2959,7 +2968,7 @@ func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error { // Mark all of these as being fully processed in our forwarding // package, which prevents us from reprocessing them after // startup. - err = c.Packager.AckAddHtlcs(tx, diff.AddAcks...) + err = channel.Packager.AckAddHtlcs(tx, diff.AddAcks...) if err != nil { return err } @@ -2969,7 +2978,9 @@ func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error { // 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 = c.Packager.AckSettleFails(tx, diff.SettleFailAcks...) + err = channel.Packager.AckSettleFails( + tx, diff.SettleFailAcks..., + ) if err != nil { return err } diff --git a/chanstate/interface.go b/chanstate/interface.go index 6a2f4d87b2..b1dce6d1e1 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -232,6 +232,11 @@ type OpenChannelCommitmentStore[Channel any] interface { UpdateChannelCommitment(channel Channel, newCommitment *ChannelCommitment, unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) + + // AppendRemoteCommitChain appends a new CommitDiff to the remote + // party's commitment chain. This is used after preparing a new remote + // commitment state, before transmitting it to the remote party. + AppendRemoteCommitChain(channel Channel, diff *CommitDiff) error } // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. From 37148d7db8c2be05d976f993a30c7be63ed35cc9 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:40:11 -0300 Subject: [PATCH 14/33] chanstate: add commit lookup store Add read-side commitment lookup methods to the chanstate commitment store facet. Move the existing OpenChannel KV view transaction bodies onto ChannelStateDB. Leave the OpenChannel receivers as store-call wrappers. This removes three more direct backend references from the receiver code without changing the persisted data format. --- channeldb/channel.go | 39 +++++++++++++++++++++++++++++++++------ chanstate/interface.go | 12 ++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index f96894c609..5daa100bfc 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3014,10 +3014,19 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, // this new pending commitment. Once they revoked their prior state, we'll swap // these pointers, causing the tip and the tail to point to the same entry. func (c *OpenChannel) RemoteCommitChainTip() (*CommitDiff, error) { + return c.Db.RemoteCommitChainTip(c) +} + +// RemoteCommitChainTip returns the "tip" of the current remote commitment +// chain. +func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( + *CommitDiff, error) { + var cd *CommitDiff - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) switch err { case nil: @@ -3053,10 +3062,19 @@ func (c *OpenChannel) RemoteCommitChainTip() (*CommitDiff, error) { // UnsignedAckedUpdates retrieves the persisted unsigned acked remote log // updates that still need to be signed for. func (c *OpenChannel) UnsignedAckedUpdates() ([]LogUpdate, error) { + return c.Db.UnsignedAckedUpdates(c) +} + +// UnsignedAckedUpdates retrieves the persisted unsigned acked remote log +// updates that still need to be signed for. +func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( + []LogUpdate, error) { + var updates []LogUpdate - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) switch err { case nil: @@ -3087,10 +3105,19 @@ func (c *OpenChannel) UnsignedAckedUpdates() ([]LogUpdate, error) { // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log // updates that the remote still needs to sign for. func (c *OpenChannel) RemoteUnsignedLocalUpdates() ([]LogUpdate, error) { + return c.Db.RemoteUnsignedLocalUpdates(c) +} + +// RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log +// updates that the remote still needs to sign for. +func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( + []LogUpdate, error) { + var updates []LogUpdate - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) switch err { case nil: diff --git a/chanstate/interface.go b/chanstate/interface.go index b1dce6d1e1..ac7c46a670 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -237,6 +237,18 @@ type OpenChannelCommitmentStore[Channel any] interface { // party's commitment chain. This is used after preparing a new remote // commitment state, before transmitting it to the remote party. AppendRemoteCommitChain(channel Channel, diff *CommitDiff) error + + // RemoteCommitChainTip returns the "tip" of the current remote + // commitment chain. + RemoteCommitChainTip(channel Channel) (*CommitDiff, error) + + // UnsignedAckedUpdates retrieves the persisted unsigned acked remote + // log updates that still need to be signed for. + UnsignedAckedUpdates(channel Channel) ([]LogUpdate, error) + + // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local + // log updates that the remote still needs to sign for. + RemoteUnsignedLocalUpdates(channel Channel) ([]LogUpdate, error) } // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. From 7231fc375c2fb8f2fc927fa23e4029fe13b6e1ba Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:46:06 -0300 Subject: [PATCH 15/33] chanstate: add revocation insert store Add the next-revocation persistence method to the chanstate commitment store facet. Move the existing OpenChannel KV update body onto ChannelStateDB. The OpenChannel receiver keeps the external locking behavior and delegates persistence through the store interface. --- channeldb/channel.go | 17 +++++++++++++---- chanstate/interface.go | 4 ++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 5daa100bfc..225d5b4ed9 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3157,17 +3157,26 @@ func (c *OpenChannel) InsertNextRevocation(revKey *btcec.PublicKey) error { c.Lock() defer c.Unlock() - c.RemoteNextRevocation = revKey + return c.Db.InsertNextRevocation(c, revKey) +} - err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { +// InsertNextRevocation inserts the next commitment point into the persisted +// channel state. +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, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - return putChanRevocationState(chanBucket, c) + return putChanRevocationState(chanBucket, channel) }, func() {}) if err != nil { return err diff --git a/chanstate/interface.go b/chanstate/interface.go index ac7c46a670..a63260d31e 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -249,6 +249,10 @@ type OpenChannelCommitmentStore[Channel any] interface { // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local // log updates that the remote still needs to sign for. RemoteUnsignedLocalUpdates(channel Channel) ([]LogUpdate, error) + + // InsertNextRevocation inserts the next commitment point into the + // persisted channel state. + InsertNextRevocation(channel Channel, revKey *btcec.PublicKey) error } // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. From 0108e34676bd0b5d087937713c675cc0a1545588 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:53:10 -0300 Subject: [PATCH 16/33] chanstate: move forwarding package types Move FwdState, PkgFilter, and FwdPkg into chanstate with their existing comments and helper methods. Leave channeldb aliases for the moved value types and constructors so current callers keep compiling. The KV forwarding package persistence code stays in channeldb. --- channeldb/forwarding_package.go | 276 ++++---------------------------- chanstate/forwarding.go | 250 +++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 248 deletions(-) diff --git a/channeldb/forwarding_package.go b/channeldb/forwarding_package.go index 8ba2c955d9..31ec1cc748 100644 --- a/channeldb/forwarding_package.go +++ b/channeldb/forwarding_package.go @@ -2,10 +2,7 @@ package channeldb import ( "bytes" - "encoding/binary" "errors" - "fmt" - "io" cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/kvdb" @@ -19,33 +16,44 @@ type ( // SettleFailRef is used to locate a Settle/Fail in another channel's // FwdPkg. SettleFailRef = cstate.SettleFailRef -) -// ErrCorruptedFwdPkg signals that the on-disk structure of the forwarding -// package has potentially been mangled. -var ErrCorruptedFwdPkg = errors.New("fwding package db has been corrupted") + // FwdState is an enum used to describe the lifecycle of a FwdPkg. + FwdState = cstate.FwdState + + // PkgFilter is used to compactly represent a particular subset of the + // Adds in a forwarding package. + PkgFilter = cstate.PkgFilter -// FwdState is an enum used to describe the lifecycle of a FwdPkg. -type FwdState byte + // 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 +) const ( // FwdStateLockedIn is the starting state for all forwarding packages. - // Packages in this state have not yet committed to the exact set of - // Adds to forward to the switch. - FwdStateLockedIn FwdState = iota + FwdStateLockedIn = cstate.FwdStateLockedIn // FwdStateProcessed marks the state in which all Adds have been - // locally processed and the forwarding decision to the switch has been - // persisted. - FwdStateProcessed - - // FwdStateCompleted signals that all Adds have been acked, and that all - // settles and fails have been delivered to their sources. Packages in - // this state can be removed permanently. - FwdStateCompleted + // locally processed. + FwdStateProcessed = cstate.FwdStateProcessed + + // FwdStateCompleted signals that all Adds have been acked, and that + // all settles and fails have been delivered to their sources. + FwdStateCompleted = cstate.FwdStateCompleted ) var ( + // NewPkgFilter initializes an empty PkgFilter supporting `count` + // elements. + NewPkgFilter = cstate.NewPkgFilter + + // NewFwdPkg initializes a new forwarding package in FwdStateLockedIn. + NewFwdPkg = cstate.NewFwdPkg + + // 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. @@ -109,234 +117,6 @@ var ( settleFailFilterKey = []byte("settle-fail-filter-key") ) -// PkgFilter is used to compactly represent a particular subset of the Adds in a -// forwarding package. Each filter is represented as a simple, statically-sized -// bitvector, where the elements are intended to be the indices of the Adds as -// they are written in the FwdPkg. -type PkgFilter struct { - count uint16 - filter []byte -} - -// NewPkgFilter initializes an empty PkgFilter supporting `count` elements. -func NewPkgFilter(count uint16) *PkgFilter { - // We add 7 to ensure that the integer division yields properly rounded - // values. - filterLen := (count + 7) / 8 - - return &PkgFilter{ - count: count, - filter: make([]byte, filterLen), - } -} - -// Count returns the number of elements represented by this PkgFilter. -func (f *PkgFilter) Count() uint16 { - return f.count -} - -// Set marks the `i`-th element as included by this filter. -// NOTE: It is assumed that i is always less than count. -func (f *PkgFilter) Set(i uint16) { - byt := i / 8 - bit := i % 8 - - // Set the i-th bit in the filter. - // TODO(conner): ignore if > count to prevent panic? - f.filter[byt] |= byte(1 << (7 - bit)) -} - -// Contains queries the filter for membership of index `i`. -// NOTE: It is assumed that i is always less than count. -func (f *PkgFilter) Contains(i uint16) bool { - byt := i / 8 - bit := i % 8 - - // Read the i-th bit in the filter. - // TODO(conner): ignore if > count to prevent panic? - return f.filter[byt]&(1<<(7-bit)) != 0 -} - -// Equal checks two PkgFilters for equality. -func (f *PkgFilter) Equal(f2 *PkgFilter) bool { - if f == f2 { - return true - } - if f.count != f2.count { - return false - } - - return bytes.Equal(f.filter, f2.filter) -} - -// IsFull returns true if every element in the filter has been Set, and false -// otherwise. -func (f *PkgFilter) IsFull() bool { - // Batch validate bytes that are fully used. - for i := uint16(0); i < f.count/8; i++ { - if f.filter[i] != 0xFF { - return false - } - } - - // If the count is not a multiple of 8, check that the filter contains - // all remaining bits. - rem := f.count % 8 - for idx := f.count - rem; idx < f.count; idx++ { - if !f.Contains(idx) { - return false - } - } - - 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) -} - -// FwdPkg records all adds, settles, and fails that were locked in as a result -// of the remote peer sending us a revocation. Each package is identified by -// the short chanid and remote commitment height corresponding to the revocation -// that locked in the HTLCs. For everything except a locally initiated payment, -// settles and fails in a forwarding package must have a corresponding Add in -// another package, and can be removed individually once the source link has -// received the fail/settle. -// -// Adds cannot be removed, as we need to present the same batch of Adds to -// properly handle replay protection. Instead, we use a PkgFilter to mark that -// we have finished processing a particular Add. A FwdPkg should only be deleted -// after the AckFilter is full and all settles and fails have been persistently -// removed. -type FwdPkg struct { - // Source identifies the channel that wrote this forwarding package. - Source lnwire.ShortChannelID - - // Height is the height of the remote commitment chain that locked in - // this forwarding package. - Height uint64 - - // State signals the persistent condition of the package and directs how - // to reprocess the package in the event of failures. - State FwdState - - // Adds contains all add messages which need to be processed and - // forwarded to the switch. Adds does not change over the life of a - // forwarding package. - Adds []LogUpdate - - // FwdFilter is a filter containing the indices of all Adds that were - // forwarded to the switch. - // - // NOTE: This value signals when persisted to disk that the fwd package - // has been processed and garbage collection can happen. So it also - // has to be set for packages with no adds (empty packages or only - // settle/fail packages) so that they can be garbage collected as well. - FwdFilter *PkgFilter - - // AckFilter is a filter containing the indices of all Adds for which - // the source has received a settle or fail and is reflected in the next - // commitment txn. A package should not be removed until IsFull() - // returns true. - AckFilter *PkgFilter - - // SettleFails contains all settle and fail messages that should be - // forwarded to the switch. - SettleFails []LogUpdate - - // SettleFailFilter is a filter containing the indices of all Settle or - // Fails originating in this package that have been received and locked - // into the incoming link's commitment state. - SettleFailFilter *PkgFilter -} - -// NewFwdPkg initializes a new forwarding package in FwdStateLockedIn. This -// should be used to create a package at the time we receive a revocation. -func NewFwdPkg(source lnwire.ShortChannelID, height uint64, - addUpdates, settleFailUpdates []LogUpdate) *FwdPkg { - - nAddUpdates := uint16(len(addUpdates)) - nSettleFailUpdates := uint16(len(settleFailUpdates)) - - return &FwdPkg{ - Source: source, - Height: height, - State: FwdStateLockedIn, - Adds: addUpdates, - FwdFilter: NewPkgFilter(nAddUpdates), - AckFilter: NewPkgFilter(nAddUpdates), - SettleFails: settleFailUpdates, - SettleFailFilter: NewPkgFilter(nSettleFailUpdates), - } -} - -// SourceRef is a convenience method that returns an AddRef to this forwarding -// package for the index in the argument. It is the caller's responsibility -// to ensure that the index is in bounds. -func (f *FwdPkg) SourceRef(i uint16) AddRef { - return AddRef{ - Height: f.Height, - Index: i, - } -} - -// DestRef is a convenience method that returns a SettleFailRef to this -// forwarding package for the index in the argument. It is the caller's -// responsibility to ensure that the index is in bounds. -func (f *FwdPkg) DestRef(i uint16) SettleFailRef { - return SettleFailRef{ - Source: f.Source, - Height: f.Height, - Index: i, - } -} - -// ID returns an unique identifier for this package, used to ensure that sphinx -// replay processing of this batch is idempotent. -func (f *FwdPkg) ID() []byte { - var id = make([]byte, 16) - byteOrder.PutUint64(id[:8], f.Source.ToUint64()) - byteOrder.PutUint64(id[8:], f.Height) - return id -} - -// String returns a human-readable description of the forwarding package. -func (f *FwdPkg) String() string { - return fmt.Sprintf("%T(src=%v, height=%v, nadds=%v, nfailsettles=%v)", - f, f.Source, f.Height, len(f.Adds), len(f.SettleFails)) -} - // SettleFailAcker is a generic interface providing the ability to acknowledge // settle/fail HTLCs stored in forwarding packages. type SettleFailAcker interface { diff --git a/chanstate/forwarding.go b/chanstate/forwarding.go index 9cc830ff12..efee78e6ea 100644 --- a/chanstate/forwarding.go +++ b/chanstate/forwarding.go @@ -1,7 +1,9 @@ package chanstate import ( + "bytes" "encoding/binary" + "fmt" "io" "github.com/lightningnetwork/lnd/lnwire" @@ -55,3 +57,251 @@ type SettleFailRef struct { // NOTE: This index is static over the lifetime of a forwarding package. Index uint16 } + +// FwdState is an enum used to describe the lifecycle of a FwdPkg. +type FwdState byte + +const ( + // FwdStateLockedIn is the starting state for all forwarding packages. + // Packages in this state have not yet committed to the exact set of + // Adds to forward to the switch. + FwdStateLockedIn FwdState = iota + + // FwdStateProcessed marks the state in which all Adds have been + // locally processed and the forwarding decision to the switch has been + // persisted. + FwdStateProcessed + + // FwdStateCompleted signals that all Adds have been acked, and that all + // settles and fails have been delivered to their sources. Packages in + // this state can be removed permanently. + FwdStateCompleted +) + +// PkgFilter is used to compactly represent a particular subset of the Adds in a +// forwarding package. Each filter is represented as a simple, statically-sized +// bitvector, where the elements are intended to be the indices of the Adds as +// they are written in the FwdPkg. +type PkgFilter struct { + count uint16 + filter []byte +} + +// NewPkgFilter initializes an empty PkgFilter supporting `count` elements. +func NewPkgFilter(count uint16) *PkgFilter { + // We add 7 to ensure that the integer division yields properly rounded + // values. + filterLen := (count + 7) / 8 + + return &PkgFilter{ + count: count, + filter: make([]byte, filterLen), + } +} + +// Count returns the number of elements represented by this PkgFilter. +func (f *PkgFilter) Count() uint16 { + return f.count +} + +// Set marks the `i`-th element as included by this filter. +// NOTE: It is assumed that i is always less than count. +func (f *PkgFilter) Set(i uint16) { + byt := i / 8 + bit := i % 8 + + // Set the i-th bit in the filter. + // TODO(conner): ignore if > count to prevent panic? + f.filter[byt] |= byte(1 << (7 - bit)) +} + +// Contains queries the filter for membership of index `i`. +// NOTE: It is assumed that i is always less than count. +func (f *PkgFilter) Contains(i uint16) bool { + byt := i / 8 + bit := i % 8 + + // Read the i-th bit in the filter. + // TODO(conner): ignore if > count to prevent panic? + return f.filter[byt]&(1<<(7-bit)) != 0 +} + +// Equal checks two PkgFilters for equality. +func (f *PkgFilter) Equal(f2 *PkgFilter) bool { + if f == f2 { + return true + } + if f.count != f2.count { + return false + } + + return bytes.Equal(f.filter, f2.filter) +} + +// IsFull returns true if every element in the filter has been Set, and false +// otherwise. +func (f *PkgFilter) IsFull() bool { + // Batch validate bytes that are fully used. + for i := uint16(0); i < f.count/8; i++ { + if f.filter[i] != 0xFF { + return false + } + } + + // If the count is not a multiple of 8, check that the filter contains + // all remaining bits. + rem := f.count % 8 + for idx := f.count - rem; idx < f.count; idx++ { + if !f.Contains(idx) { + return false + } + } + + 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) +} + +// FwdPkg records all adds, settles, and fails that were locked in as a result +// of the remote peer sending us a revocation. Each package is identified by +// the short chanid and remote commitment height corresponding to the revocation +// that locked in the HTLCs. For everything except a locally initiated payment, +// settles and fails in a forwarding package must have a corresponding Add in +// another package, and can be removed individually once the source link has +// received the fail/settle. +// +// Adds cannot be removed, as we need to present the same batch of Adds to +// properly handle replay protection. Instead, we use a PkgFilter to mark that +// we have finished processing a particular Add. A FwdPkg should only be deleted +// after the AckFilter is full and all settles and fails have been persistently +// removed. +type FwdPkg struct { + // Source identifies the channel that wrote this forwarding package. + Source lnwire.ShortChannelID + + // Height is the height of the remote commitment chain that locked in + // this forwarding package. + Height uint64 + + // State signals the persistent condition of the package and directs how + // to reprocess the package in the event of failures. + State FwdState + + // Adds contains all add messages which need to be processed and + // forwarded to the switch. Adds does not change over the life of a + // forwarding package. + Adds []LogUpdate + + // FwdFilter is a filter containing the indices of all Adds that were + // forwarded to the switch. + // + // NOTE: This value signals when persisted to disk that the fwd package + // has been processed and garbage collection can happen. So it also + // has to be set for packages with no adds (empty packages or only + // settle/fail packages) so that they can be garbage collected as well. + FwdFilter *PkgFilter + + // AckFilter is a filter containing the indices of all Adds for which + // the source has received a settle or fail and is reflected in the next + // commitment txn. A package should not be removed until IsFull() + // returns true. + AckFilter *PkgFilter + + // SettleFails contains all settle and fail messages that should be + // forwarded to the switch. + SettleFails []LogUpdate + + // SettleFailFilter is a filter containing the indices of all Settle or + // Fails originating in this package that have been received and locked + // into the incoming link's commitment state. + SettleFailFilter *PkgFilter +} + +// NewFwdPkg initializes a new forwarding package in FwdStateLockedIn. This +// should be used to create a package at the time we receive a revocation. +func NewFwdPkg(source lnwire.ShortChannelID, height uint64, + addUpdates, settleFailUpdates []LogUpdate) *FwdPkg { + + nAddUpdates := uint16(len(addUpdates)) + nSettleFailUpdates := uint16(len(settleFailUpdates)) + + return &FwdPkg{ + Source: source, + Height: height, + State: FwdStateLockedIn, + Adds: addUpdates, + FwdFilter: NewPkgFilter(nAddUpdates), + AckFilter: NewPkgFilter(nAddUpdates), + SettleFails: settleFailUpdates, + SettleFailFilter: NewPkgFilter(nSettleFailUpdates), + } +} + +// SourceRef is a convenience method that returns an AddRef to this forwarding +// package for the index in the argument. It is the caller's responsibility +// to ensure that the index is in bounds. +func (f *FwdPkg) SourceRef(i uint16) AddRef { + return AddRef{ + Height: f.Height, + Index: i, + } +} + +// DestRef is a convenience method that returns a SettleFailRef to this +// forwarding package for the index in the argument. It is the caller's +// responsibility to ensure that the index is in bounds. +func (f *FwdPkg) DestRef(i uint16) SettleFailRef { + return SettleFailRef{ + Source: f.Source, + Height: f.Height, + Index: i, + } +} + +// ID returns an unique identifier for this package, used to ensure that sphinx +// replay processing of this batch is idempotent. +func (f *FwdPkg) ID() []byte { + var id = make([]byte, 16) + binary.BigEndian.PutUint64(id[:8], f.Source.ToUint64()) + binary.BigEndian.PutUint64(id[8:], f.Height) + return id +} + +// String returns a human-readable description of the forwarding package. +func (f *FwdPkg) String() string { + return fmt.Sprintf("%T(src=%v, height=%v, nadds=%v, nfailsettles=%v)", + f, f.Source, f.Height, len(f.Adds), len(f.SettleFails)) +} From e41944b4ffe4d40a2d610d23785f8d745ada6319 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 18:59:30 -0300 Subject: [PATCH 17/33] chanstate: add commit tail store Add the commitment-tail advancement method to the chanstate commitment store facet. Move the existing AdvanceCommitChainTail KV transaction body onto ChannelStateDB. The OpenChannel receiver now keeps locking and restored channel checks before delegating persistence through the store. --- channeldb/channel.go | 29 +++++++++++++++++++++-------- chanstate/interface.go | 7 +++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 225d5b4ed9..8773964811 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3207,11 +3207,24 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, return ErrNoRestoredChannelMutation } + return c.Db.AdvanceCommitChainTail( + c, fwdPkg, updates, ourOutputIndex, theirOutputIndex, + ) +} + +// AdvanceCommitChainTail records the new state transition within the +// revocation log and promotes the pending remote commitment to the current +// remote commitment. +func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, + fwdPkg *FwdPkg, updates []LogUpdate, ourOutputIndex, + theirOutputIndex uint32) error { + var newRemoteCommit *ChannelCommitment - err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + err := kvdb.Update(c.backend, func(tx kvdb.RwTx) error { chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err @@ -3219,7 +3232,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // If the channel is marked as borked, then for safety reasons, // we shouldn't attempt any further updates. - isBorked, err := c.isBorked(chanBucket) + isBorked, err := channel.isBorked(chanBucket) if err != nil { return err } @@ -3230,7 +3243,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // 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. - if err := putChanRevocationState(chanBucket, c); err != nil { + if err := putChanRevocationState(chanBucket, channel); err != nil { return err } @@ -3269,8 +3282,8 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // With the commitment pointer swapped, we can now add the // revoked (prior) state to the revocation log. err = putRevocationLog( - logBucket, &c.RemoteCommitment, ourOutputIndex, - theirOutputIndex, c.Db.parent.noRevLogAmtData, + logBucket, &channel.RemoteCommitment, ourOutputIndex, + theirOutputIndex, c.parent.noRevLogAmtData, ) if err != nil { return err @@ -3279,7 +3292,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // 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. - if err := c.Packager.AddFwdPkg(tx, fwdPkg); err != nil { + if err := channel.Packager.AddFwdPkg(tx, fwdPkg); err != nil { return err } @@ -3351,7 +3364,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // 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. - c.RemoteCommitment = *newRemoteCommit + channel.RemoteCommitment = *newRemoteCommit return nil } diff --git a/chanstate/interface.go b/chanstate/interface.go index a63260d31e..e5db55e5e2 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -253,6 +253,13 @@ type OpenChannelCommitmentStore[Channel any] interface { // InsertNextRevocation inserts the next commitment point into the // persisted channel state. InsertNextRevocation(channel Channel, revKey *btcec.PublicKey) error + + // AdvanceCommitChainTail records the new state transition within the + // revocation log and promotes the pending remote commitment to the + // current remote commitment. + AdvanceCommitChainTail(channel Channel, fwdPkg *FwdPkg, + updates []LogUpdate, ourOutputIndex, + theirOutputIndex uint32) error } // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. From da2fdf1e7624e1a30475b467b3ddeafe0c3c6d9b Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:04:29 -0300 Subject: [PATCH 18/33] chanstate: add forwarding package store Add a forwarding-package store facet to chanstate.Store. Move the existing OpenChannel forwarding-package KV transaction bodies onto ChannelStateDB. The OpenChannel receivers keep their locking behavior and delegate package loading, acking, filtering, and removal through the store. --- channeldb/channel.go | 67 +++++++++++++++++++++++++++++++++++------- chanstate/interface.go | 25 ++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 8773964811..19e45e17b5 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3421,10 +3421,19 @@ func (c *OpenChannel) LoadFwdPkgs() ([]*FwdPkg, error) { c.RLock() defer c.RUnlock() + return c.Db.LoadFwdPkgs(c) +} + +// 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 (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, + error) { + var fwdPkgs []*FwdPkg - if err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { var err error - fwdPkgs, err = c.Packager.LoadFwdPkgs(tx) + fwdPkgs, err = channel.Packager.LoadFwdPkgs(tx) return err }, func() { fwdPkgs = nil @@ -3442,8 +3451,17 @@ func (c *OpenChannel) AckAddHtlcs(addRefs ...AddRef) error { c.Lock() defer c.Unlock() - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return c.Packager.AckAddHtlcs(tx, addRefs...) + return c.Db.AckAddHtlcs(c, addRefs...) +} + +// 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 (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, + addRefs ...AddRef) error { + + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + return channel.Packager.AckAddHtlcs(tx, addRefs...) }, func() {}) } @@ -3455,8 +3473,18 @@ func (c *OpenChannel) AckSettleFails(settleFailRefs ...SettleFailRef) error { c.Lock() defer c.Unlock() - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return c.Packager.AckSettleFails(tx, settleFailRefs...) + return c.Db.AckSettleFails(c, settleFailRefs...) +} + +// 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 (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, + settleFailRefs ...SettleFailRef) error { + + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + return channel.Packager.AckSettleFails(tx, settleFailRefs...) }, func() {}) } @@ -3466,8 +3494,16 @@ func (c *OpenChannel) SetFwdFilter(height uint64, fwdFilter *PkgFilter) error { c.Lock() defer c.Unlock() - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return c.Packager.SetFwdFilter(tx, height, fwdFilter) + return c.Db.SetFwdFilter(c, height, fwdFilter) +} + +// SetFwdFilter atomically sets the forwarding filter for the forwarding package +// identified by `height`. +func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, + fwdFilter *PkgFilter) error { + + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + return channel.Packager.SetFwdFilter(tx, height, fwdFilter) }, func() {}) } @@ -3480,9 +3516,20 @@ func (c *OpenChannel) RemoveFwdPkgs(heights ...uint64) error { c.Lock() defer c.Unlock() - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { + return c.Db.RemoveFwdPkgs(c, heights...) +} + +// 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 (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, + heights ...uint64) error { + + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { for _, height := range heights { - err := c.Packager.RemovePkg(tx, height) + err := channel.Packager.RemovePkg(tx, height) if err != nil { return err } diff --git a/chanstate/interface.go b/chanstate/interface.go index e5db55e5e2..ac60676b44 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -45,6 +45,10 @@ type Store[Channel any] interface { // channel records. OpenChannelCommitmentStore[Channel] + // OpenChannelFwdPkgStore owns forwarding packages tied to open + // channel records. + OpenChannelFwdPkgStore[Channel] + // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. ClosedChannelStore[Channel] @@ -262,6 +266,27 @@ type OpenChannelCommitmentStore[Channel any] interface { theirOutputIndex uint32) error } +// OpenChannelFwdPkgStore owns forwarding packages tied to open channel records. +type OpenChannelFwdPkgStore[Channel any] interface { + // LoadFwdPkgs loads forwarding packages that have not been processed. + LoadFwdPkgs(channel Channel) ([]*FwdPkg, error) + + // AckAddHtlcs marks add HTLCs in forwarding packages as resolved. + AckAddHtlcs(channel Channel, addRefs ...AddRef) error + + // AckSettleFails marks settles or fails as delivered to the incoming + // link. + AckSettleFails(channel Channel, settleFailRefs ...SettleFailRef) error + + // SetFwdFilter writes the forwarding filter for the forwarding package + // identified by height. + SetFwdFilter(channel Channel, height uint64, fwdFilter *PkgFilter) error + + // RemoveFwdPkgs removes forwarding packages by remote commitment + // height. + RemoveFwdPkgs(channel Channel, heights ...uint64) error +} + // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. type ClosedChannelStore[Channel any] interface { // FetchClosedChannels attempts to fetch all closed channels from the From b60dd7816445a1b0d438518d3d68a9012acb6e49 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:09:22 -0300 Subject: [PATCH 19/33] chanstate: add commitment read stores Add commitment-height, latest-commitment, and remote revocation store lookups to the chanstate commitment store facet. Move the existing OpenChannel KV view transaction bodies onto ChannelStateDB. This leaves the receivers as store-call wrappers while keeping the persisted format and read behavior unchanged. --- channeldb/channel.go | 54 ++++++++++++++++++++++++++++++++++-------- chanstate/interface.go | 13 ++++++++++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 19e45e17b5..024b7ef233 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3595,12 +3595,24 @@ func (c *OpenChannel) CommitmentHeight() (uint64, error) { c.RLock() defer c.RUnlock() + return c.Db.CommitmentHeight(c) +} + +// 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 (c *ChannelStateDB) CommitmentHeight(channel *OpenChannel) ( + uint64, error) { + var height uint64 - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + 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, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err @@ -4089,21 +4101,32 @@ func (c *OpenChannel) Copy() *OpenChannel { // latest fully committed state is returned. The first commitment returned is // the local commitment, and the second returned is the remote commitment. func (c *OpenChannel) LatestCommitments() (*ChannelCommitment, *ChannelCommitment, error) { - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + return c.Db.LatestCommitments(c) +} + +// 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 (c *ChannelStateDB) LatestCommitments(channel *OpenChannel) ( + *ChannelCommitment, *ChannelCommitment, error) { + + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - return fetchChanCommitments(chanBucket, c) + return fetchChanCommitments(chanBucket, channel) }, func() {}) if err != nil { return nil, nil, err } - return &c.LocalCommitment, &c.RemoteCommitment, nil + return &channel.LocalCommitment, &channel.RemoteCommitment, nil } // RemoteRevocationStore returns the most up to date commitment version of the @@ -4111,21 +4134,32 @@ func (c *OpenChannel) LatestCommitments() (*ChannelCommitment, *ChannelCommitmen // acting on a possible contract breach to ensure, that the caller has the most // up to date information required to deliver justice. func (c *OpenChannel) RemoteRevocationStore() (shachain.Store, error) { - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + return c.Db.RemoteRevocationStore(c) +} + +// 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 (c *ChannelStateDB) RemoteRevocationStore(channel *OpenChannel) ( + shachain.Store, error) { + + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err } - return fetchChanRevocationState(chanBucket, c) + return fetchChanRevocationState(chanBucket, channel) }, func() {}) if err != nil { return nil, err } - return c.RevocationStore, nil + return channel.RevocationStore, nil } // AbsoluteThawHeight determines a frozen channel's absolute thaw height. If the diff --git a/chanstate/interface.go b/chanstate/interface.go index ac60676b44..a24f7261b5 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -9,6 +9,7 @@ import ( "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" ) // Store is the full persistence contract for the channel-state subsystem. @@ -264,6 +265,18 @@ type OpenChannelCommitmentStore[Channel any] interface { AdvanceCommitChainTail(channel Channel, fwdPkg *FwdPkg, updates []LogUpdate, ourOutputIndex, theirOutputIndex uint32) error + + // CommitmentHeight returns the current persisted commitment height. + CommitmentHeight(channel Channel) (uint64, error) + + // LatestCommitments returns the two latest commitments for both the + // local and remote party. + LatestCommitments(channel Channel) (*ChannelCommitment, + *ChannelCommitment, error) + + // RemoteRevocationStore returns the most up to date commitment version + // of the revocation storage tree for the remote party. + RemoteRevocationStore(channel Channel) (shachain.Store, error) } // OpenChannelFwdPkgStore owns forwarding packages tied to open channel records. From c1db1f2ddeb0bf6ad0ed59defab05163a1f33100 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:12:34 -0300 Subject: [PATCH 20/33] channeldb: move revocation log reads Move the remaining OpenChannel revocation-log KV reads onto ChannelStateDB. This keeps FindPreviousState and the unit-test tail-height helper as OpenChannel wrappers. It removes direct backend access from the receiver methods while leaving RevocationLog in channeldb for now. --- channeldb/channel.go | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 024b7ef233..b3402d9ffd 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3549,17 +3549,26 @@ func (c *OpenChannel) revocationLogTailCommitHeight() (uint64, error) { c.RLock() defer c.RUnlock() + return c.Db.revocationLogTailCommitHeight(c) +} + +// revocationLogTailCommitHeight returns the commit height at the end of the +// revocation log. +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 c.RemoteCommitment.CommitHeight == 0 { + if channel.RemoteCommitment.CommitHeight == 0 { return height, nil } - if err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err @@ -3646,12 +3655,24 @@ func (c *OpenChannel) FindPreviousState( c.RLock() defer c.RUnlock() + return c.Db.FindPreviousState(c, updateNum) +} + +// 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 (c *ChannelStateDB) FindPreviousState(channel *OpenChannel, + updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { + commit := &ChannelCommitment{} rl := &RevocationLog{} - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(c.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( - tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, + tx, channel.IdentityPub, &channel.FundingOutpoint, + channel.ChainHash, ) if err != nil { return err From 4990b1f327645dc979b71baeeef60c789a9b8b93 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:26:47 -0300 Subject: [PATCH 21/33] chanstate: move revocation log types Move the revocation-log value types and TLV serialization helpers into chanstate. Leave channeldb aliases and wrapper functions for the existing KV persistence code and tests. Bucket keys, errors, and transaction helpers stay in channeldb, so this commit only moves backend-neutral state data. --- channeldb/revocation_log.go | 541 +++-------------------------------- chanstate/revocation_log.go | 556 ++++++++++++++++++++++++++++++++++++ 2 files changed, 590 insertions(+), 507 deletions(-) create mode 100644 chanstate/revocation_log.go diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 5a7f7a76be..8340c8b19b 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -2,34 +2,54 @@ package channeldb import ( "bytes" - "encoding/binary" "errors" "io" "math" - "github.com/btcsuite/btcd/btcutil" - "github.com/lightningnetwork/lnd/fn/v2" + cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/kvdb" - "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" ) const ( // OutputIndexEmpty is used when the output index doesn't exist. - OutputIndexEmpty = math.MaxUint16 + OutputIndexEmpty = cstate.OutputIndexEmpty ) type ( // BigSizeAmount is a type alias for a TLV record of a btcutil.Amount. - BigSizeAmount = tlv.BigSizeT[btcutil.Amount] + BigSizeAmount = cstate.BigSizeAmount // BigSizeMilliSatoshi is a type alias for a TLV record of a // lnwire.MilliSatoshi. - BigSizeMilliSatoshi = tlv.BigSizeT[lnwire.MilliSatoshi] + BigSizeMilliSatoshi = cstate.BigSizeMilliSatoshi + + // SparsePayHash is a type alias for a 32 byte array, which when + // serialized is able to save some space by not including an empty + // payment hash on disk. + SparsePayHash = cstate.SparsePayHash + + // 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. + HTLCEntry = cstate.HTLCEntry + + // RevocationLog stores the info needed to construct a breach + // retribution. + RevocationLog = cstate.RevocationLog ) var ( + // NewSparsePayHash creates a new SparsePayHash from a 32 byte array. + NewSparsePayHash = cstate.NewSparsePayHash + + // NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC. + NewHTLCEntryFromHTLC = cstate.NewHTLCEntryFromHTLC + + // NewRevocationLog creates a new RevocationLog from the given + // parameters. + NewRevocationLog = cstate.NewRevocationLog + // 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 @@ -55,266 +75,6 @@ var ( ErrOutputIndexTooBig = errors.New("output index is over uint16") ) -// SparsePayHash is a type alias for a 32 byte array, which when serialized is -// able to save some space by not including an empty payment hash on disk. -type SparsePayHash [32]byte - -// NewSparsePayHash creates a new SparsePayHash from a 32 byte array. -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. -// The actual size of each HTLCEntry varies based on its RHash and Amt(sat), -// summarized as follows, -// -// | RHash empty | Amt<=252 | Amt<=65,535 | Amt<=4,294,967,295 | otherwise | -// |:-----------:|:--------:|:-----------:|:------------------:|:---------:| -// | true | 19 | 21 | 23 | 26 | -// | false | 51 | 53 | 55 | 58 | -// -// So the size varies from 19 bytes to 58 bytes, where most likely to be 23 or -// 55 bytes. -// -// NOTE: all the fields saved to disk use the primitive go types so they can be -// made into tlv records without further conversion. -type HTLCEntry struct { - // RHash is the payment hash of the HTLC. - RHash tlv.RecordT[tlv.TlvType0, SparsePayHash] - - // RefundTimeout is the absolute timeout on the HTLC that the sender - // must wait before reclaiming the funds in limbo. - RefundTimeout tlv.RecordT[tlv.TlvType1, uint32] - - // OutputIndex is the output index for this particular HTLC output - // within the commitment transaction. - // - // NOTE: we use uint16 instead of int32 here to save us 2 bytes, which - // gives us a max number of HTLCs of 65K. - OutputIndex tlv.RecordT[tlv.TlvType2, uint16] - - // Incoming denotes whether we're the receiver or the sender of this - // HTLC. - Incoming tlv.RecordT[tlv.TlvType3, bool] - - // Amt is the amount of satoshis this HTLC escrows. - Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]] - - // CustomBlob is an optional blob that can be used to store information - // specific to revocation handling for a custom channel type. - CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] - - // HtlcIndex is the index of the HTLC in the channel. - 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{ - RHash: tlv.NewRecordT[tlv.TlvType0]( - NewSparsePayHash(htlc.RHash), - ), - RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1]( - htlc.RefundTimeout, - ), - OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2]( - uint16(htlc.OutputIndex), - ), - Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](htlc.Incoming), - Amt: tlv.NewRecordT[tlv.TlvType4]( - tlv.NewBigSizeT(htlc.Amt.ToSatoshis()), - ), - HtlcIndex: tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( - tlv.NewBigSizeT(htlc.HtlcIndex), - )), - } - - if len(htlc.CustomRecords) != 0 { - blob, err := htlc.CustomRecords.Serialize() - if err != nil { - return nil, err - } - - h.CustomBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), - ) - } - - return h, nil -} - -// RevocationLog stores the info needed to construct a breach retribution. Its -// fields can be viewed as a subset of a ChannelCommitment's. In the database, -// all historical versions of the RevocationLog are saved using the -// CommitHeight as the key. -type RevocationLog struct { - // OurOutputIndex specifies our output index in this commitment. In a - // remote commitment transaction, this is the to remote output index. - OurOutputIndex tlv.RecordT[tlv.TlvType0, uint16] - - // TheirOutputIndex specifies their output index in this commitment. In - // a remote commitment transaction, this is the to local output index. - TheirOutputIndex tlv.RecordT[tlv.TlvType1, uint16] - - // CommitTxHash is the hash of the latest version of the commitment - // state, broadcast able by us. - CommitTxHash tlv.RecordT[tlv.TlvType2, [32]byte] - - // HTLCEntries is the set of HTLCEntry's that are pending at this - // particular commitment height. - HTLCEntries []*HTLCEntry - - // OurBalance is the current available balance within the channel - // directly spendable by us. In other words, it is the value of the - // to_remote output on the remote parties' commitment transaction. - // - // NOTE: this is an option so that it is clear if the value is zero or - // nil. Since migration 30 of the channeldb initially did not include - // this field, it could be the case that the field is not present for - // all revocation logs. - OurBalance tlv.OptionalRecordT[tlv.TlvType3, BigSizeMilliSatoshi] - - // TheirBalance is the current available balance within the channel - // directly spendable by the remote node. In other words, it is the - // value of the to_local output on the remote parties' commitment. - // - // NOTE: this is an option so that it is clear if the value is zero or - // nil. Since migration 30 of the channeldb initially did not include - // this field, it could be the case that the field is not present for - // all revocation logs. - TheirBalance tlv.OptionalRecordT[tlv.TlvType4, BigSizeMilliSatoshi] - - // CustomBlob is an optional blob that can be used to store information - // specific to a custom channel type. This information is only created - // at channel funding time, and after wards is to be considered - // immutable. - CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] -} - -// NewRevocationLog creates a new RevocationLog from the given parameters. -func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, - commitHash [32]byte, ourBalance, - theirBalance fn.Option[lnwire.MilliSatoshi], htlcs []*HTLCEntry, - customBlob fn.Option[tlv.Blob]) RevocationLog { - - rl := RevocationLog{ - OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0]( - ourOutputIndex, - ), - TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1]( - theirOutputIndex, - ), - CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2](commitHash), - HTLCEntries: htlcs, - } - - ourBalance.WhenSome(func(balance lnwire.MilliSatoshi) { - rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3]( - tlv.NewBigSizeT(balance), - )) - }) - - theirBalance.WhenSome(func(balance lnwire.MilliSatoshi) { - rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4]( - tlv.NewBigSizeT(balance), - )) - }) - - customBlob.WhenSome(func(blob tlv.Blob) { - rl.CustomBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), - ) - }) - - return rl -} - // 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 @@ -407,269 +167,36 @@ func fetchRevocationLog(log kvdb.RBucket, // 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) + return cstate.SerializeRevocationLog(w, rl) } // 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 + return cstate.SerializeHTLCEntries(w, htlcs) } // 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 + return cstate.DeserializeRevocationLog(r) } // 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 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 + return cstate.DeserializeHTLCEntries(r) } // 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 + return cstate.WriteTlvStream(w, s) } // 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 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) + return cstate.ReadTlvStream(r, s) } // fetchOldRevocationLog finds the revocation log from the deprecated diff --git a/chanstate/revocation_log.go b/chanstate/revocation_log.go new file mode 100644 index 0000000000..712b8ae9ac --- /dev/null +++ b/chanstate/revocation_log.go @@ -0,0 +1,556 @@ +package chanstate + +import ( + "bytes" + "encoding/binary" + "io" + "math" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // OutputIndexEmpty is used when the output index doesn't exist. + OutputIndexEmpty = math.MaxUint16 +) + +type ( + // BigSizeAmount is a type alias for a TLV record of a btcutil.Amount. + BigSizeAmount = tlv.BigSizeT[btcutil.Amount] + + // BigSizeMilliSatoshi is a type alias for a TLV record of a + // lnwire.MilliSatoshi. + BigSizeMilliSatoshi = tlv.BigSizeT[lnwire.MilliSatoshi] +) + +// SparsePayHash is a type alias for a 32 byte array, which when serialized is +// able to save some space by not including an empty payment hash on disk. +type SparsePayHash [32]byte + +// NewSparsePayHash creates a new SparsePayHash from a 32 byte array. +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. +// The actual size of each HTLCEntry varies based on its RHash and Amt(sat), +// summarized as follows, +// +// | RHash empty | Amt<=252 | Amt<=65,535 | Amt<=4,294,967,295 | otherwise | +// |:-----------:|:--------:|:-----------:|:------------------:|:---------:| +// | true | 19 | 21 | 23 | 26 | +// | false | 51 | 53 | 55 | 58 | +// +// So the size varies from 19 bytes to 58 bytes, where most likely to be 23 or +// 55 bytes. +// +// NOTE: all the fields saved to disk use the primitive go types so they can be +// made into tlv records without further conversion. +type HTLCEntry struct { + // RHash is the payment hash of the HTLC. + RHash tlv.RecordT[tlv.TlvType0, SparsePayHash] + + // RefundTimeout is the absolute timeout on the HTLC that the sender + // must wait before reclaiming the funds in limbo. + RefundTimeout tlv.RecordT[tlv.TlvType1, uint32] + + // OutputIndex is the output index for this particular HTLC output + // within the commitment transaction. + // + // NOTE: we use uint16 instead of int32 here to save us 2 bytes, which + // gives us a max number of HTLCs of 65K. + OutputIndex tlv.RecordT[tlv.TlvType2, uint16] + + // Incoming denotes whether we're the receiver or the sender of this + // HTLC. + Incoming tlv.RecordT[tlv.TlvType3, bool] + + // Amt is the amount of satoshis this HTLC escrows. + Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]] + + // CustomBlob is an optional blob that can be used to store information + // specific to revocation handling for a custom channel type. + CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] + + // HtlcIndex is the index of the HTLC in the channel. + 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{ + RHash: tlv.NewRecordT[tlv.TlvType0]( + NewSparsePayHash(htlc.RHash), + ), + RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1]( + htlc.RefundTimeout, + ), + OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2]( + uint16(htlc.OutputIndex), + ), + Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](htlc.Incoming), + Amt: tlv.NewRecordT[tlv.TlvType4]( + tlv.NewBigSizeT(htlc.Amt.ToSatoshis()), + ), + HtlcIndex: tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType6]( + tlv.NewBigSizeT(htlc.HtlcIndex), + )), + } + + if len(htlc.CustomRecords) != 0 { + blob, err := htlc.CustomRecords.Serialize() + if err != nil { + return nil, err + } + + h.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + } + + return h, nil +} + +// RevocationLog stores the info needed to construct a breach retribution. Its +// fields can be viewed as a subset of a ChannelCommitment's. In the database, +// all historical versions of the RevocationLog are saved using the +// CommitHeight as the key. +type RevocationLog struct { + // OurOutputIndex specifies our output index in this commitment. In a + // remote commitment transaction, this is the to remote output index. + OurOutputIndex tlv.RecordT[tlv.TlvType0, uint16] + + // TheirOutputIndex specifies their output index in this commitment. In + // a remote commitment transaction, this is the to local output index. + TheirOutputIndex tlv.RecordT[tlv.TlvType1, uint16] + + // CommitTxHash is the hash of the latest version of the commitment + // state, broadcast able by us. + CommitTxHash tlv.RecordT[tlv.TlvType2, [32]byte] + + // HTLCEntries is the set of HTLCEntry's that are pending at this + // particular commitment height. + HTLCEntries []*HTLCEntry + + // OurBalance is the current available balance within the channel + // directly spendable by us. In other words, it is the value of the + // to_remote output on the remote parties' commitment transaction. + // + // NOTE: this is an option so that it is clear if the value is zero or + // nil. Since migration 30 of the channeldb initially did not include + // this field, it could be the case that the field is not present for + // all revocation logs. + OurBalance tlv.OptionalRecordT[tlv.TlvType3, BigSizeMilliSatoshi] + + // TheirBalance is the current available balance within the channel + // directly spendable by the remote node. In other words, it is the + // value of the to_local output on the remote parties' commitment. + // + // NOTE: this is an option so that it is clear if the value is zero or + // nil. Since migration 30 of the channeldb initially did not include + // this field, it could be the case that the field is not present for + // all revocation logs. + TheirBalance tlv.OptionalRecordT[tlv.TlvType4, BigSizeMilliSatoshi] + + // CustomBlob is an optional blob that can be used to store information + // specific to a custom channel type. This information is only created + // at channel funding time, and after wards is to be considered + // immutable. + CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] +} + +// NewRevocationLog creates a new RevocationLog from the given parameters. +func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, + commitHash [32]byte, ourBalance, + theirBalance fn.Option[lnwire.MilliSatoshi], htlcs []*HTLCEntry, + customBlob fn.Option[tlv.Blob]) RevocationLog { + + rl := RevocationLog{ + OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0]( + ourOutputIndex, + ), + TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1]( + theirOutputIndex, + ), + CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2](commitHash), + HTLCEntries: htlcs, + } + + ourBalance.WhenSome(func(balance lnwire.MilliSatoshi) { + rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3]( + tlv.NewBigSizeT(balance), + )) + }) + + theirBalance.WhenSome(func(balance lnwire.MilliSatoshi) { + rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4]( + tlv.NewBigSizeT(balance), + )) + }) + + customBlob.WhenSome(func(blob tlv.Blob) { + rl.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + }) + + 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 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 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 bc7d8bd5f06c61e387b4638861a3a7b067e51fb3 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:30:20 -0300 Subject: [PATCH 22/33] chanstate: add previous state lookup Add FindPreviousState to the chanstate commitment store facet now that RevocationLog is a chanstate value type. This extends the store contract without changing runtime behavior. The existing ChannelStateDB method already satisfies the new method. --- chanstate/interface.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chanstate/interface.go b/chanstate/interface.go index a24f7261b5..be073c87a1 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -277,6 +277,11 @@ type OpenChannelCommitmentStore[Channel any] interface { // RemoteRevocationStore returns the most up to date commitment version // of the revocation storage tree for the remote party. RemoteRevocationStore(channel Channel) (shachain.Store, error) + + // FindPreviousState scans through the append-only log in an attempt to + // recover the previous channel state indicated by the update number. + FindPreviousState(channel Channel, updateNum uint64) ( + *RevocationLog, *ChannelCommitment, error) } // OpenChannelFwdPkgStore owns forwarding packages tied to open channel records. From 4c3046ae5b2c3b37b2e36a76f8ac5b79297b5855 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:34:51 -0300 Subject: [PATCH 23/33] channeldb: move revocation tail helper Keep the revocation-log tail-height helper on ChannelStateDB instead of the OpenChannel receiver. The helper is only used by channeldb tests, so it should not become part of the backend-independent chanstate store contract. The tests now call the concrete helper directly. --- channeldb/channel.go | 13 ------------- channeldb/channel_test.go | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index b3402d9ffd..caa7b4a2da 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3539,19 +3539,6 @@ func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, }, func() {}) } -// revocationLogTailCommitHeight returns the commit height at the end of the -// revocation log. This entry represents the last previous state for the remote -// node's commitment chain. The ChannelDelta returned by this method will -// always lag one state behind the most current (unrevoked) state of the remote -// node's commitment chain. -// NOTE: used in unit test only. -func (c *OpenChannel) revocationLogTailCommitHeight() (uint64, error) { - c.RLock() - defer c.RUnlock() - - return c.Db.revocationLogTailCommitHeight(c) -} - // revocationLogTailCommitHeight returns the commit height at the end of the // revocation log. func (c *ChannelStateDB) revocationLogTailCommitHeight( diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 4750406778..c19843165e 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -879,7 +879,7 @@ func TestChannelStateTransition(t *testing.T) { // The state number recovered from the tail of the revocation log // should be identical to this current state. - logTailHeight, err := channel.revocationLogTailCommitHeight() + logTailHeight, err := cdb.revocationLogTailCommitHeight(channel) require.NoError(t, err, "unable to retrieve log") if logTailHeight != oldRemoteCommit.CommitHeight { t.Fatal("update number doesn't match") @@ -922,7 +922,7 @@ func TestChannelStateTransition(t *testing.T) { // Once again, state number recovered from the tail of the revocation // log should be identical to this current state. - logTailHeight, err = channel.revocationLogTailCommitHeight() + logTailHeight, err = cdb.revocationLogTailCommitHeight(channel) require.NoError(t, err, "unable to retrieve log") if logTailHeight != oldRemoteCommit.CommitHeight { t.Fatal("update number doesn't match") From f94d05b1ea60ddfd30490f9eac0e03925bf1f6d6 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:41:30 -0300 Subject: [PATCH 24/33] channeldb: store channel state by interface Change OpenChannel.Db to the composed chanstate Store interface while keeping the existing field name. Tests that need raw channeldb access now assert the concrete test backend explicitly instead of reaching through OpenChannel.Db. This keeps backend setup out of the store contract. --- channeldb/channel.go | 7 +++-- channeldb/close_channel_test.go | 34 +++++++++++++++---------- contractcourt/breach_arbitrator_test.go | 14 +++++----- contractcourt/utils_test.go | 17 ++++++++++++- htlcswitch/link_test.go | 4 +-- htlcswitch/test_utils.go | 23 +++++++++++++---- 6 files changed, 69 insertions(+), 30 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index caa7b4a2da..aeff51ffb1 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -801,8 +801,11 @@ type OpenChannel struct { // immutable. CustomBlob fn.Option[tlv.Blob] - // TODO(roasbeef): eww - Db *ChannelStateDB + // Db persists channel state through the chanstate Store contract. This + // field intentionally keeps the existing name while the code moves from + // channeldb toward chanstate so call sites can become backend + // independent before the OpenChannel type itself is moved. + Db cstate.Store[*OpenChannel] // TODO(roasbeef): just need to store local and remote HTLC's? diff --git a/channeldb/close_channel_test.go b/channeldb/close_channel_test.go index 3a940b78de..43466a44b1 100644 --- a/channeldb/close_channel_test.go +++ b/channeldb/close_channel_test.go @@ -15,10 +15,12 @@ import ( // revocationLogBucket of the given channel. The helper navigates the raw KV // tree so the test does not depend on the higher-level commit-chain // machinery. -func writeTestRevlogEntries(t *testing.T, ch *OpenChannel, n int) { +func writeTestRevlogEntries(t *testing.T, cdb *ChannelStateDB, + ch *OpenChannel, n int) { + t.Helper() - err := kvdb.Update(ch.Db.backend, func(tx kvdb.RwTx) error { + err := kvdb.Update(cdb.backend, func(tx kvdb.RwTx) error { openChanBkt := tx.ReadWriteBucket(openChannelBucket) require.NotNil(t, openChanBkt, "openChannelBucket missing") @@ -56,11 +58,13 @@ func writeTestRevlogEntries(t *testing.T, ch *OpenChannel, n int) { // writeTestForwardingPackages writes n empty forwarding packages for the // given channel using distinct remote commitment heights. -func writeTestForwardingPackages(t *testing.T, ch *OpenChannel, n int) { +func writeTestForwardingPackages(t *testing.T, cdb *ChannelStateDB, + ch *OpenChannel, n int) { + t.Helper() packager := NewChannelPackager(ch.ShortChanID()) - err := kvdb.Update(ch.Db.backend, func(tx kvdb.RwTx) error { + err := kvdb.Update(cdb.backend, func(tx kvdb.RwTx) error { for i := range n { pkg := NewFwdPkg( ch.ShortChanID(), uint64(i), nil, nil, @@ -78,11 +82,13 @@ func writeTestForwardingPackages(t *testing.T, ch *OpenChannel, n int) { // countRevlogEntries returns the number of entries in the revocationLogBucket // for the given channel, or -1 if the channel bucket no longer exists in // openChannelBucket. -func countRevlogEntries(t *testing.T, ch *OpenChannel) int { +func countRevlogEntries(t *testing.T, cdb *ChannelStateDB, + ch *OpenChannel) int { + t.Helper() count := -1 - err := kvdb.View(ch.Db.backend, func(tx kvdb.RTx) error { + err := kvdb.View(cdb.backend, func(tx kvdb.RTx) error { openChanBkt := tx.ReadBucket(openChannelBucket) if openChanBkt == nil { return nil @@ -202,8 +208,8 @@ func TestCloseChannelTombstoneWritePath(t *testing.T) { const numRevlogEntries = 5 const numFwdPkgs = 3 - writeTestRevlogEntries(t, ch, numRevlogEntries) - writeTestForwardingPackages(t, ch, numFwdPkgs) + writeTestRevlogEntries(t, cdb, ch, numRevlogEntries) + writeTestForwardingPackages(t, cdb, ch, numFwdPkgs) closeChannelForTest(t, cdb, ch) @@ -224,7 +230,7 @@ func TestCloseChannelTombstoneWritePath(t *testing.T) { require.Equal(t, ch.FundingOutpoint, closeSummary.ChanPoint) // Bulk state preserved on disk — tombstoning's whole point. - require.Equal(t, numRevlogEntries, countRevlogEntries(t, ch)) + require.Equal(t, numRevlogEntries, countRevlogEntries(t, cdb, ch)) packager := NewChannelPackager(ch.ShortChanID()) var fwdPkgs []*FwdPkg @@ -281,7 +287,7 @@ func TestCloseChannelTombstoneRemovesFromOpenScans(t *testing.T) { ch2 := createTestChannel(t, cdb, openChannelOption()) const numRevlogEntries = 5 - writeTestRevlogEntries(t, ch1, numRevlogEntries) + writeTestRevlogEntries(t, cdb, ch1, numRevlogEntries) openChans, err := cdb.FetchAllChannels() require.NoError(t, err) @@ -313,7 +319,7 @@ func TestCloseChannelTombstoneRemovesFromOpenScans(t *testing.T) { // The bulk historical state stays put — that is the whole point of // the tombstone path on these backends. - require.Equal(t, numRevlogEntries, countRevlogEntries(t, ch1)) + require.Equal(t, numRevlogEntries, countRevlogEntries(t, cdb, ch1)) // The outpoint index for ch1 must flip to closed; ch2's stays open. require.Equal(t, outpointClosed, readOutpointStatus( @@ -380,14 +386,14 @@ func TestCloseChannelSync(t *testing.T) { ch := createTestChannel(t, cdb, openChannelOption()) const numRevlogEntries = 4 - writeTestRevlogEntries(t, ch, numRevlogEntries) - writeTestForwardingPackages(t, ch, 3) + writeTestRevlogEntries(t, cdb, ch, numRevlogEntries) + writeTestForwardingPackages(t, cdb, ch, 3) closeChannelForTest(t, cdb, ch) // The synchronous path wipes the chanBucket inline, so // countRevlogEntries must report -1 (bucket is gone, not just empty). - require.Equal(t, -1, countRevlogEntries(t, ch), + require.Equal(t, -1, countRevlogEntries(t, cdb, ch), "channel bucket must be deleted after sync close") // Forwarding packages are wiped inline. diff --git a/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go index 869a0093e0..cd764a13a4 100644 --- a/contractcourt/breach_arbitrator_test.go +++ b/contractcourt/breach_arbitrator_test.go @@ -952,7 +952,8 @@ func initBreachedState(t *testing.T) (*BreachArbitrator, contractBreaches := make(chan *ContractBreachEvent) brar, err := createTestArbiter( - t, contractBreaches, alice.State().Db.GetParentDB(), + t, contractBreaches, + testChannelStateDB(t, alice.State()).GetParentDB(), ) require.NoError(t, err, "unable to initialize test breach arbiter") @@ -1118,7 +1119,8 @@ func TestBreachHandoffFail(t *testing.T) { assertNotPendingClosed(t, alice) brar, err := createTestArbiter( - t, contractBreaches, alice.State().Db.GetParentDB(), + t, contractBreaches, + testChannelStateDB(t, alice.State()).GetParentDB(), ) require.NoError(t, err, "unable to initialize test breach arbiter") @@ -1763,7 +1765,7 @@ func testBreachSpends(t *testing.T, test breachTest) { } // Assert that the channel is fully resolved. - assertBrarCleanup(t, brar, &chanPoint, alice.State().Db) + assertBrarCleanup(t, brar, &chanPoint, testChannelStateDB(t, alice.State())) } // TestBreachDelayedJusticeConfirmation tests that the breach arbiter will @@ -1968,7 +1970,7 @@ func TestBreachDelayedJusticeConfirmation(t *testing.T) { } // Assert that the channel is fully resolved. - assertBrarCleanup(t, brar, &chanPoint, alice.State().Db) + assertBrarCleanup(t, brar, &chanPoint, testChannelStateDB(t, alice.State())) } // findInputIndex returns the index of the input that spends from the given @@ -2080,7 +2082,7 @@ func assertBrarCleanup(t *testing.T, brar *BreachArbitrator, func assertPendingClosed(t *testing.T, c *lnwallet.LightningChannel) { t.Helper() - closedChans, err := c.State().Db.FetchClosedChannels(true) + closedChans, err := testChannelStateDB(t, c.State()).FetchClosedChannels(true) require.NoError(t, err, "unable to load pending closed channels") for _, chanSummary := range closedChans { @@ -2097,7 +2099,7 @@ func assertPendingClosed(t *testing.T, c *lnwallet.LightningChannel) { func assertNotPendingClosed(t *testing.T, c *lnwallet.LightningChannel) { t.Helper() - closedChans, err := c.State().Db.FetchClosedChannels(true) + closedChans, err := testChannelStateDB(t, c.State()).FetchClosedChannels(true) require.NoError(t, err, "unable to load pending closed channels") for _, chanSummary := range closedChans { diff --git a/contractcourt/utils_test.go b/contractcourt/utils_test.go index 994bc57a88..22c62217ea 100644 --- a/contractcourt/utils_test.go +++ b/contractcourt/utils_test.go @@ -12,6 +12,19 @@ import ( "github.com/lightningnetwork/lnd/channeldb" ) +func testChannelStateDB(t testing.TB, + state *channeldb.OpenChannel) *channeldb.ChannelStateDB { + + t.Helper() + + cdb, ok := state.Db.(*channeldb.ChannelStateDB) + if !ok { + t.Fatalf("expected ChannelStateDB, got %T", state.Db) + } + + return cdb +} + // timeout implements a test level timeout. func timeout() func() { done := make(chan struct{}) @@ -56,7 +69,9 @@ func copyChannelState(t *testing.T, state *channeldb.OpenChannel) ( *channeldb.OpenChannel, error) { // Make a copy of the DB. - dbFile := filepath.Join(state.Db.GetParentDB().Path(), "channel.db") + dbFile := filepath.Join( + testChannelStateDB(t, state).GetParentDB().Path(), "channel.db", + ) tempDbPath := t.TempDir() tempDbFile := filepath.Join(tempDbPath, "channel.db") diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 29b4f902d0..c366cf10fc 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -2174,7 +2174,7 @@ func newSingleLinkTestHarness(t *testing.T, chanAmt, pCache := newMockPreimageCache() - aliceDb := aliceLc.channel.State().Db.GetParentDB() + aliceDb := testChannelStateDB(t, aliceLc.channel).GetParentDB() aliceSwitch, err := initSwitchWithDB(testStartingHeight, aliceDb) if err != nil { return singleLinkTestHarness{}, err @@ -4854,7 +4854,7 @@ func (h *persistentLinkHarness) restartLink( pCache = newMockPreimageCache() ) - aliceDb := aliceChannel.State().Db.GetParentDB() + aliceDb := testChannelStateDB(t, aliceChannel).GetParentDB() if restartSwitch { var err error h.hSwitch, err = initSwitchWithDB(testStartingHeight, aliceDb) diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index 2e08425094..5f24a8ae41 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -43,6 +43,19 @@ import ( "github.com/stretchr/testify/require" ) +func testChannelStateDB(t testing.TB, + channel *lnwallet.LightningChannel) *channeldb.ChannelStateDB { + + t.Helper() + + cdb, ok := channel.State().Db.(*channeldb.ChannelStateDB) + if !ok { + t.Fatalf("expected ChannelStateDB, got %T", channel.State().Db) + } + + return cdb +} + // maxInflightHtlcs specifies the max number of inflight HTLCs. This number is // chosen to be smaller than the default 483 so the test can run faster. const maxInflightHtlcs = 50 @@ -954,9 +967,9 @@ func newThreeHopNetwork(t testing.TB, aliceChannel, firstBobChannel, secondBobChannel, carolChannel *lnwallet.LightningChannel, startingHeight uint32, opts ...serverOption) *threeHopNetwork { - aliceDb := aliceChannel.State().Db.GetParentDB() - bobDb := firstBobChannel.State().Db.GetParentDB() - carolDb := carolChannel.State().Db.GetParentDB() + aliceDb := testChannelStateDB(t, aliceChannel).GetParentDB() + bobDb := testChannelStateDB(t, firstBobChannel).GetParentDB() + carolDb := testChannelStateDB(t, carolChannel).GetParentDB() hopNetwork := newHopNetwork() @@ -1233,8 +1246,8 @@ func newTwoHopNetwork(t testing.TB, aliceChannel, bobChannel *lnwallet.LightningChannel, startingHeight uint32) *twoHopNetwork { - aliceDb := aliceChannel.State().Db.GetParentDB() - bobDb := bobChannel.State().Db.GetParentDB() + aliceDb := testChannelStateDB(t, aliceChannel).GetParentDB() + bobDb := testChannelStateDB(t, bobChannel).GetParentDB() hopNetwork := newHopNetwork() From 89ef03bbe1ebcbb5f08275492ce0d04605d4ecf1 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 14 May 2026 19:46:45 -0300 Subject: [PATCH 25/33] channeldb: split out channel kv helpers Convert the KV-only OpenChannel helpers for TLV aux data and borked-state lookup into package-level channeldb helpers. This keeps serialization and bucket inspection code tied to the KV backend while leaving the OpenChannel receiver set closer to the future chanstate type. --- channeldb/channel.go | 68 +++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index aeff51ffb1..f9c6336d22 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -962,68 +962,70 @@ func (c *OpenChannel) SetBroadcastHeight(height uint32) { c.FundingBroadcastHeight = height } -// amendTlvData updates the channel with the given auxiliary TLV data. -func (c *OpenChannel) amendTlvData(auxData openChannelTlvData) { - c.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator - c.InitialLocalBalance = lnwire.MilliSatoshi( +// 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, ) - c.InitialRemoteBalance = lnwire.MilliSatoshi( + channel.InitialRemoteBalance = lnwire.MilliSatoshi( auxData.initialRemoteBalance.Val, ) - c.confirmedScid = auxData.realScid.Val - c.ConfirmationHeight = auxData.confirmationHeight.Val + channel.confirmedScid = auxData.realScid.Val + channel.ConfirmationHeight = auxData.confirmationHeight.Val auxData.memo.WhenSomeV(func(memo []byte) { - c.Memo = memo + channel.Memo = memo }) auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) { - c.TapscriptRoot = fn.Some[chainhash.Hash](h) + channel.TapscriptRoot = fn.Some[chainhash.Hash](h) }) auxData.customBlob.WhenSomeV(func(blob tlv.Blob) { - c.CustomBlob = fn.Some(blob) + channel.CustomBlob = fn.Some(blob) }) auxData.closeConfirmationHeight.WhenSomeV(func(h uint32) { - c.CloseConfirmationHeight = fn.Some(h) + channel.CloseConfirmationHeight = fn.Some(h) }) } -// extractTlvData creates a new openChannelTlvData from the given channel. -func (c *OpenChannel) extractTlvData() openChannelTlvData { +// extractOpenChannelTlvData creates a new openChannelTlvData from the given +// channel. +func extractOpenChannelTlvData(channel *OpenChannel) openChannelTlvData { auxData := openChannelTlvData{ revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1]( - keyLocRecord{c.RevocationKeyLocator}, + keyLocRecord{channel.RevocationKeyLocator}, ), initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2]( - uint64(c.InitialLocalBalance), + uint64(channel.InitialLocalBalance), ), initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3]( - uint64(c.InitialRemoteBalance), + uint64(channel.InitialRemoteBalance), ), realScid: tlv.NewRecordT[tlv.TlvType4]( - c.confirmedScid, + channel.confirmedScid, ), confirmationHeight: tlv.NewPrimitiveRecord[tlv.TlvType8]( - c.ConfirmationHeight, + channel.ConfirmationHeight, ), } - if len(c.Memo) != 0 { + if len(channel.Memo) != 0 { auxData.memo = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType5](c.Memo), + tlv.NewPrimitiveRecord[tlv.TlvType5](channel.Memo), ) } - c.TapscriptRoot.WhenSome(func(h chainhash.Hash) { + channel.TapscriptRoot.WhenSome(func(h chainhash.Hash) { auxData.tapscriptRoot = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h), ) }) - c.CustomBlob.WhenSome(func(blob tlv.Blob) { + channel.CustomBlob.WhenSome(func(blob tlv.Blob) { auxData.customBlob = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType7](blob), ) }) - c.CloseConfirmationHeight.WhenSome(func(h uint32) { + channel.CloseConfirmationHeight.WhenSome(func(h uint32) { auxData.closeConfirmationHeight = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType9](h), ) @@ -1904,18 +1906,20 @@ func (c *ChannelStateDB) FetchChannelShutdownInfo( return fn.Some[ShutdownInfo](*shutdownInfo), nil } -// isBorked returns true if the channel has been marked as borked in the +// 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 (c *OpenChannel) isBorked(chanBucket kvdb.RBucket) (bool, error) { - channel, err := fetchOpenChannel(chanBucket, &c.FundingOutpoint) +func isChannelBorked(channel *OpenChannel, chanBucket kvdb.RBucket) ( + bool, error) { + + diskChannel, err := fetchOpenChannel(chanBucket, &channel.FundingOutpoint) if err != nil { return false, err } - return channel.chanStatus != ChanStatusDefault, nil + return diskChannel.chanStatus != ChanStatusDefault, nil } // MarkCommitmentBroadcasted marks the channel as a commitment transaction has @@ -2364,7 +2368,7 @@ func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, // If the channel is marked as borked, then for safety reasons, // we shouldn't attempt any further updates. - isBorked, err := channel.isBorked(chanBucket) + isBorked, err := isChannelBorked(channel, chanBucket) if err != nil { return err } @@ -2958,7 +2962,7 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, // If the channel is marked as borked, then for safety reasons, // we shouldn't attempt any further updates. - isBorked, err := channel.isBorked(chanBucket) + isBorked, err := isChannelBorked(channel, chanBucket) if err != nil { return err } @@ -3235,7 +3239,7 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, // If the channel is marked as borked, then for safety reasons, // we shouldn't attempt any further updates. - isBorked, err := channel.isBorked(chanBucket) + isBorked, err := isChannelBorked(channel, chanBucket) if err != nil { return err } @@ -4449,7 +4453,7 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { return err } - auxData := channel.extractTlvData() + auxData := extractOpenChannelTlvData(channel) if err := auxData.encode(&w); err != nil { return fmt.Errorf("unable to encode aux data: %w", err) } @@ -4653,7 +4657,7 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { // Assign all the relevant fields from the aux data into the actual // open channel. - channel.amendTlvData(auxData) + amendOpenChannelTlvData(channel, auxData) channel.Packager = NewChannelPackager(channel.ShortChannelID) From 96755dc5360bed71918a9cfb07cfdf9639808fc1 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 09:54:16 -0300 Subject: [PATCH 26/33] channeldb: add channel store accessors Add transitional OpenChannel accessors for the channel status and confirmed SCID fields used by KV store code. These helpers keep the fields private while allowing channeldb backend code to continue hydrating and serializing channel state after OpenChannel moves to chanstate. --- channeldb/channel.go | 72 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index f9c6336d22..144b8f8f96 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -908,6 +908,27 @@ func (c *OpenChannel) ChanStatus() ChannelStatus { return c.chanStatus } +// ChannelStatusForStore returns the in-memory channel status without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb while OpenChannel moves toward chanstate. Callers +// are responsible for synchronization. Normal callers should use ChanStatus. +func (c *OpenChannel) ChannelStatusForStore() ChannelStatus { + return c.chanStatus +} + +// SetChannelStatusForStore updates the in-memory channel status without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb while OpenChannel moves toward chanstate. Callers +// are responsible for synchronization. Normal callers should use +// ApplyChanStatus or ClearChanStatus when the status change must be persisted. +func (c *OpenChannel) SetChannelStatusForStore(status ChannelStatus) { + c.chanStatus = status +} + // ApplyChanStatus allows the caller to modify the internal channel state in a // thead-safe manner. func (c *OpenChannel) ApplyChanStatus(status ChannelStatus) error { @@ -946,6 +967,27 @@ func (c *OpenChannel) hasChanStatus(status ChannelStatus) bool { return c.chanStatus&status == status } +// ConfirmedScidForStore returns the in-memory confirmed SCID without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb while OpenChannel moves toward chanstate. Callers +// are responsible for synchronization. Normal callers should use +// ZeroConfRealScid. +func (c *OpenChannel) ConfirmedScidForStore() lnwire.ShortChannelID { + return c.confirmedScid +} + +// SetConfirmedScidForStore updates the in-memory confirmed SCID without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb while OpenChannel moves toward chanstate. Callers +// are responsible for synchronization. +func (c *OpenChannel) SetConfirmedScidForStore(scid lnwire.ShortChannelID) { + c.confirmedScid = scid +} + // BroadcastHeight returns the height at which the funding tx was broadcast. func (c *OpenChannel) BroadcastHeight() uint32 { c.RLock() @@ -972,7 +1014,7 @@ func amendOpenChannelTlvData(channel *OpenChannel, auxData openChannelTlvData) { channel.InitialRemoteBalance = lnwire.MilliSatoshi( auxData.initialRemoteBalance.Val, ) - channel.confirmedScid = auxData.realScid.Val + channel.SetConfirmedScidForStore(auxData.realScid.Val) channel.ConfirmationHeight = auxData.confirmationHeight.Val auxData.memo.WhenSomeV(func(memo []byte) { @@ -1003,7 +1045,7 @@ func extractOpenChannelTlvData(channel *OpenChannel) openChannelTlvData { uint64(channel.InitialRemoteBalance), ), realScid: tlv.NewRecordT[tlv.TlvType4]( - channel.confirmedScid, + channel.ConfirmedScidForStore(), ), confirmationHeight: tlv.NewPrimitiveRecord[tlv.TlvType8]( channel.ConfirmationHeight, @@ -1483,7 +1525,7 @@ func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, return err } - diskChannel.confirmedScid = realScid + diskChannel.SetConfirmedScidForStore(realScid) return putOpenChannel(chanBucket, diskChannel) }, func() {}) @@ -1919,7 +1961,7 @@ func isChannelBorked(channel *OpenChannel, chanBucket kvdb.RBucket) ( return false, err } - return diskChannel.chanStatus != ChanStatusDefault, nil + return diskChannel.ChannelStatusForStore() != ChanStatusDefault, nil } // MarkCommitmentBroadcasted marks the channel as a commitment transaction has @@ -2101,8 +2143,8 @@ func (c *ChannelStateDB) putChanStatus(channel *OpenChannel, } // Add this status to the existing bitvector found in the DB. - status = diskChannel.chanStatus | status - diskChannel.chanStatus = status + status = diskChannel.ChannelStatusForStore() | status + diskChannel.SetChannelStatusForStore(status) if err := putOpenChannel(chanBucket, diskChannel); err != nil { return err @@ -2125,7 +2167,7 @@ func (c *ChannelStateDB) putChanStatus(channel *OpenChannel, } // Update the in-memory representation to keep it in sync with the DB. - channel.chanStatus = status + channel.SetChannelStatusForStore(status) return nil } @@ -2152,8 +2194,8 @@ func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, } // Unset this bit in the bitvector on disk. - status = diskChannel.chanStatus & ^status - diskChannel.chanStatus = status + status = diskChannel.ChannelStatusForStore() & ^status + diskChannel.SetChannelStatusForStore(status) return putOpenChannel(chanBucket, diskChannel) }, func() {}); err != nil { @@ -2161,7 +2203,7 @@ func (c *ChannelStateDB) ClearChannelStatus(channel *OpenChannel, } // Update the in-memory representation to keep it in sync with the DB. - channel.chanStatus = status + channel.SetChannelStatusForStore(status) return nil } @@ -3864,7 +3906,9 @@ func archiveClosedChannel(tx kvdb.RwTx, chanKey []byte, } for _, s := range statuses { - chanState.chanStatus |= s + chanState.SetChannelStatusForStore( + chanState.ChannelStatusForStore() | s, + ) } if err := putOpenChannel(historicalChanBucket, chanState); err != nil { @@ -4430,7 +4474,7 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { if err := WriteElements(&w, channel.ChanType, channel.ChainHash, channel.FundingOutpoint, channel.ShortChannelID, channel.IsPending, channel.IsInitiator, - channel.chanStatus, channel.FundingBroadcastHeight, + channel.ChannelStatusForStore(), channel.FundingBroadcastHeight, channel.NumConfsRequired, channel.ChannelFlags, channel.IdentityPub, channel.Capacity, channel.TotalMSatSent, channel.TotalMSatReceived, @@ -4609,16 +4653,18 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { } r := bytes.NewReader(infoBytes) + var chanStatus ChannelStatus if err := ReadElements(r, &channel.ChanType, &channel.ChainHash, &channel.FundingOutpoint, &channel.ShortChannelID, &channel.IsPending, &channel.IsInitiator, - &channel.chanStatus, &channel.FundingBroadcastHeight, + &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. From 0d9020a737751c201efcaa1f2705c71319e0f685 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 10:00:37 -0300 Subject: [PATCH 27/33] channeldb: derive channel packagers Remove the KV forwarding packager from OpenChannel and derive a ChannelPackager inside the channeldb store methods that need one. This keeps the backend-specific kvdb transaction helper in channeldb, so the OpenChannel type no longer carries that dependency toward chanstate. --- channeldb/channel.go | 41 +++++++++++----------- channeldb/channel_test.go | 29 ++++------------ contractcourt/breach_arbitrator_test.go | 2 -- htlcswitch/link_test.go | 46 +++++-------------------- htlcswitch/test_utils.go | 2 -- lnwallet/taproot_test_vectors_test.go | 10 ++---- lnwallet/test_utils.go | 2 -- lnwallet/transactions_test.go | 2 -- peer/test_utils.go | 2 -- 9 files changed, 38 insertions(+), 98 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 144b8f8f96..c22d591b24 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -742,11 +742,6 @@ type OpenChannel struct { // implementation of secret store is shachain store. RevocationStore shachain.Store - // Packager is used to create and update forwarding packages for this - // channel, which encodes all necessary information to recover from - // failures and reforward HTLCs that were not fully processed. - Packager FwdPackager - // FundingTxn is the transaction containing this channel's funding // outpoint. Upon restarts, this txn will be rebroadcast if the channel // is found to be pending. @@ -1457,7 +1452,6 @@ func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error { c.IsPending = false c.ShortChannelID = openLoc - c.Packager = NewChannelPackager(openLoc) return nil } @@ -2285,8 +2279,6 @@ func fetchOpenChannel(chanBucket kvdb.RBucket, err) } - channel.Packager = NewChannelPackager(channel.ShortChannelID) - return channel, nil } @@ -2966,6 +2958,10 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) { return &d, nil } +func newChannelPackager(channel *OpenChannel) *ChannelPackager { + return NewChannelPackager(channel.ShortChannelID) +} + // AppendRemoteCommitChain appends a new CommitDiff to the end of the // commitment chain for the remote party. This method is to be used once we // have prepared a new commitment state for the remote party, but before we @@ -3017,7 +3013,9 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, // Mark all of these as being fully processed in our forwarding // package, which prevents us from reprocessing them after // startup. - err = channel.Packager.AckAddHtlcs(tx, diff.AddAcks...) + packager := newChannelPackager(channel) + + err = packager.AckAddHtlcs(tx, diff.AddAcks...) if err != nil { return err } @@ -3027,7 +3025,7 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, // 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 = channel.Packager.AckSettleFails( + err = packager.AckSettleFails( tx, diff.SettleFailAcks..., ) if err != nil { @@ -3341,7 +3339,7 @@ func (c *ChannelStateDB) AdvanceCommitChainTail(channel *OpenChannel, // 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. - if err := channel.Packager.AddFwdPkg(tx, fwdPkg); err != nil { + if err := newChannelPackager(channel).AddFwdPkg(tx, fwdPkg); err != nil { return err } @@ -3482,7 +3480,7 @@ func (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, var fwdPkgs []*FwdPkg if err := kvdb.View(c.backend, func(tx kvdb.RTx) error { var err error - fwdPkgs, err = channel.Packager.LoadFwdPkgs(tx) + fwdPkgs, err = newChannelPackager(channel).LoadFwdPkgs(tx) return err }, func() { fwdPkgs = nil @@ -3510,7 +3508,7 @@ func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, addRefs ...AddRef) error { return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - return channel.Packager.AckAddHtlcs(tx, addRefs...) + return newChannelPackager(channel).AckAddHtlcs(tx, addRefs...) }, func() {}) } @@ -3533,7 +3531,9 @@ func (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, settleFailRefs ...SettleFailRef) error { return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - return channel.Packager.AckSettleFails(tx, settleFailRefs...) + return newChannelPackager(channel).AckSettleFails( + tx, settleFailRefs..., + ) }, func() {}) } @@ -3552,7 +3552,9 @@ func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, fwdFilter *PkgFilter) error { return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { - return channel.Packager.SetFwdFilter(tx, height, fwdFilter) + return newChannelPackager(channel).SetFwdFilter( + tx, height, fwdFilter, + ) }, func() {}) } @@ -3577,8 +3579,10 @@ func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, heights ...uint64) error { return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + packager := newChannelPackager(channel) + for _, height := range heights { - err := channel.Packager.RemovePkg(tx, height) + err := packager.RemovePkg(tx, height) if err != nil { return err } @@ -3941,7 +3945,7 @@ func (c *ChannelStateDB) closeChannelSync(channel *OpenChannel, return err } - if err = chanState.Packager.Wipe(tx); err != nil { + if err = newChannelPackager(chanState).Wipe(tx); err != nil { return err } @@ -4114,7 +4118,6 @@ func (c *OpenChannel) Copy() *OpenChannel { RemoteNextRevocation: c.RemoteNextRevocation, RevocationProducer: c.RevocationProducer, RevocationStore: c.RevocationStore, - Packager: c.Packager, ThawHeight: c.ThawHeight, LastWasRevoke: c.LastWasRevoke, RevocationKeyLocator: c.RevocationKeyLocator, @@ -4705,8 +4708,6 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { // open channel. amendOpenChannelTlvData(channel, auxData) - channel.Packager = NewChannelPackager(channel.ShortChannelID) - // Finally, read the optional shutdown scripts. if err := getOptionalUpfrontShutdownScript( chanBucket, localUpfrontShutdownKey, &channel.LocalShutdownScript, diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index c19843165e..8f155bfc9a 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -414,7 +414,6 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { RevocationProducer: producer, RevocationStore: store, Db: cdb, - Packager: NewChannelPackager(chanID), FundingTxn: channels.TestFundingTx, ThawHeight: uint32(defaultPendingHeight), InitialLocalBalance: lnwire.MilliSatoshi(9000), @@ -939,7 +938,9 @@ func TestChannelStateTransition(t *testing.T) { } // At this point, we should have 2 forwarding packages added. - fwdPkgs := loadFwdPkgs(t, cdb.backend, channel.Packager) + fwdPkgs := loadFwdPkgs( + t, cdb.backend, NewChannelPackager(channel.ShortChanID()), + ) require.Len(t, fwdPkgs, 2, "wrong number of forwarding packages") // Now attempt to delete the channel from the database. @@ -974,7 +975,9 @@ func TestChannelStateTransition(t *testing.T) { } // All forwarding packages of this channel has been deleted too. - fwdPkgs = loadFwdPkgs(t, cdb.backend, channel.Packager) + fwdPkgs = loadFwdPkgs( + t, cdb.backend, NewChannelPackager(channel.ShortChanID()), + ) require.Empty(t, fwdPkgs, "no forwarding packages should exist") } @@ -1424,16 +1427,6 @@ func TestRefresh(t *testing.T) { "updated before refreshing short_chan_id") } - // Now that the receiver's short channel id has been updated, check to - // ensure that the channel packager's source has been updated as well. - // This ensures that the packager will read and write to buckets - // corresponding to the new short chan id, instead of the prior. - if state.Packager.(*ChannelPackager).source != chanOpenLoc { - t.Fatalf("channel packager source was not updated: want %v, "+ - "got %v", chanOpenLoc, - state.Packager.(*ChannelPackager).source) - } - // Now, refresh the state of the pending channel. err = pendingChannel.Refresh() require.NoError(t, err, "unable to refresh short_chan_id") @@ -1446,16 +1439,6 @@ func TestRefresh(t *testing.T) { pendingChannel.ShortChanID()) } - // Check to ensure that the _other_ OpenChannel channel packager's - // source has also been updated after the refresh. This ensures that the - // other packagers will read and write to buckets corresponding to the - // updated short chan id. - if pendingChannel.Packager.(*ChannelPackager).source != chanOpenLoc { - t.Fatalf("channel packager source was not updated: want %v, "+ - "got %v", chanOpenLoc, - pendingChannel.Packager.(*ChannelPackager).source) - } - // Check to ensure that this channel is no longer pending and this field // is up to date. if pendingChannel.IsPending { diff --git a/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go index cd764a13a4..2d07e646c6 100644 --- a/contractcourt/breach_arbitrator_test.go +++ b/contractcourt/breach_arbitrator_test.go @@ -2322,7 +2322,6 @@ func createInitChannels(t *testing.T) ( LocalCommitment: aliceCommit, RemoteCommitment: aliceCommit, Db: dbAlice.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), FundingTxn: channels.TestFundingTx, } bobChannelState := &channeldb.OpenChannel{ @@ -2340,7 +2339,6 @@ func createInitChannels(t *testing.T) ( LocalCommitment: bobCommit, RemoteCommitment: bobCommit, Db: dbBob.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), } aliceSigner := input.NewMockSigner( diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index c366cf10fc..e54f620b64 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -25,6 +25,7 @@ import ( sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" + cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" @@ -32,7 +33,6 @@ import ( "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/input" invpkg "github.com/lightningnetwork/lnd/invoices" - "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" @@ -5769,42 +5769,14 @@ func TestChannelLinkCleanupSpuriousResponses(t *testing.T) { } } -type mockPackager struct { - failLoadFwdPkgs bool +type mockFailLoadFwdPkgStore struct { + cstate.Store[*channeldb.OpenChannel] } -func (*mockPackager) AddFwdPkg(tx kvdb.RwTx, fwdPkg *channeldb.FwdPkg) error { - return nil -} - -func (*mockPackager) SetFwdFilter(tx kvdb.RwTx, height uint64, - fwdFilter *channeldb.PkgFilter) error { - return nil -} - -func (*mockPackager) AckAddHtlcs(tx kvdb.RwTx, - addRefs ...channeldb.AddRef) error { - return nil -} - -func (m *mockPackager) LoadFwdPkgs(tx kvdb.RTx) ([]*channeldb.FwdPkg, error) { - if m.failLoadFwdPkgs { - return nil, fmt.Errorf("failing LoadFwdPkgs") - } - return nil, nil -} - -func (*mockPackager) RemovePkg(tx kvdb.RwTx, height uint64) error { - return nil -} +func (m *mockFailLoadFwdPkgStore) LoadFwdPkgs( + *channeldb.OpenChannel) ([]*channeldb.FwdPkg, error) { -func (*mockPackager) Wipe(tx kvdb.RwTx) error { - return nil -} - -func (*mockPackager) AckSettleFails(tx kvdb.RwTx, - settleFailRefs ...channeldb.SettleFailRef) error { - return nil + return nil, fmt.Errorf("failing LoadFwdPkgs") } // TestChannelLinkFail tests that we will fail the channel, and force close the @@ -5880,10 +5852,10 @@ func TestChannelLinkFail(t *testing.T) { func(c *channelLink) { // We make the call to resolveFwdPkgs fail by // making the underlying forwarder fail. - pkg := &mockPackager{ - failLoadFwdPkgs: true, + state := c.channel.State() + state.Db = &mockFailLoadFwdPkgStore{ + Store: state.Db, } - c.channel.State().Packager = pkg }, func(*testing.T, *Switch, *channelLink, *lnwallet.LightningChannel) { diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index 5f24a8ae41..796a250641 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -319,7 +319,6 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, RemoteCommitment: aliceCommit, ShortChannelID: chanID, Db: dbAlice.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(chanID), FundingTxn: channels.TestFundingTx, } @@ -338,7 +337,6 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, RemoteCommitment: bobCommit, ShortChannelID: chanID, Db: dbBob.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(chanID), } if err := aliceChannelState.SyncPending(bobAddr, broadcastHeight); err != nil { diff --git a/lnwallet/taproot_test_vectors_test.go b/lnwallet/taproot_test_vectors_test.go index 7e3dcf8e6a..1c6e26fa81 100644 --- a/lnwallet/taproot_test_vectors_test.go +++ b/lnwallet/taproot_test_vectors_test.go @@ -906,10 +906,7 @@ func createTaprootTestChannelsForVectors(tc *taprootTestContext, LocalCommitment: remoteCommit, RemoteCommitment: remoteCommit, Db: dbRemote.ChannelStateDB(), - Packager: channeldb.NewChannelPackager( - shortChanID, - ), - FundingTxn: fundingTx, + FundingTxn: fundingTx, } localChannelState := &channeldb.OpenChannel{ LocalChanCfg: localCfg, @@ -926,10 +923,7 @@ func createTaprootTestChannelsForVectors(tc *taprootTestContext, LocalCommitment: localCommit, RemoteCommitment: localCommit, Db: dbLocal.ChannelStateDB(), - Packager: channeldb.NewChannelPackager( - shortChanID, - ), - FundingTxn: fundingTx, + FundingTxn: fundingTx, } // Create mock signers with all deterministic keys. The funding key must diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index 738558e224..dec99d8941 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -323,7 +323,6 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, LocalCommitment: aliceLocalCommit, RemoteCommitment: aliceRemoteCommit, Db: dbAlice.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), FundingTxn: testTx, } bobChannelState := &channeldb.OpenChannel{ @@ -341,7 +340,6 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, LocalCommitment: bobLocalCommit, RemoteCommitment: bobRemoteCommit, Db: dbBob.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), } // If the channel type has a tapscript root, then we'll also specify diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 38131eaa72..d740565c1e 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -985,7 +985,6 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp LocalCommitment: remoteCommit, RemoteCommitment: remoteCommit, Db: dbRemote.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), FundingTxn: tc.fundingTx.MsgTx(), } localChannelState := &channeldb.OpenChannel{ @@ -1003,7 +1002,6 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp LocalCommitment: localCommit, RemoteCommitment: localCommit, Db: dbLocal.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), FundingTxn: tc.fundingTx.MsgTx(), } diff --git a/peer/test_utils.go b/peer/test_utils.go index 670af09460..7aa4c96fa5 100644 --- a/peer/test_utils.go +++ b/peer/test_utils.go @@ -253,7 +253,6 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a, LocalCommitment: aliceCommit, RemoteCommitment: aliceCommit, Db: dbAlice.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), FundingTxn: channels.TestFundingTx, } bobChannelState := &channeldb.OpenChannel{ @@ -270,7 +269,6 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a, LocalCommitment: bobCommit, RemoteCommitment: bobCommit, Db: dbBob.ChannelStateDB(), - Packager: channeldb.NewChannelPackager(shortChanID), } // Set custom values on the channel states. From 2b24b4969a7c3233a4a8f60907fa26414f23e5a7 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 10:03:31 -0300 Subject: [PATCH 28/33] chanstate: move channel snapshot type Move the backend-neutral ChannelSnapshot value type into chanstate and leave channeldb with a compatibility alias. This keeps the future OpenChannel Snapshot receiver close to its return type without changing existing channeldb callers. --- channeldb/channel.go | 36 ++--------------------------------- chanstate/snapshot.go | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 chanstate/snapshot.go diff --git a/channeldb/channel.go b/channeldb/channel.go index c22d591b24..81a0ac2e42 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -4012,40 +4012,8 @@ func (c *ChannelStateDB) closeChannelTombstone(channel *OpenChannel, }, func() {}) } -// ChannelSnapshot is a frozen snapshot of the current channel state. A -// snapshot is detached from the original channel that generated it, providing -// read-only access to the current or prior state of an active channel. -// -// TODO(roasbeef): remove all together? pretty much just commitment -type ChannelSnapshot struct { - // RemoteIdentity is the identity public key of the remote node that we - // are maintaining the open channel with. - RemoteIdentity btcec.PublicKey - - // ChanPoint is the outpoint that created the channel. This output is - // found within the funding transaction and uniquely identified the - // channel on the resident chain. - ChannelPoint wire.OutPoint - - // ChainHash is the genesis hash of the chain that the channel resides - // within. - ChainHash chainhash.Hash - - // Capacity is the total capacity of the channel. - Capacity btcutil.Amount - - // TotalMSatSent is the total number of milli-satoshis we've sent - // within this channel. - TotalMSatSent lnwire.MilliSatoshi - - // TotalMSatReceived is the total number of milli-satoshis we've - // received within this channel. - TotalMSatReceived lnwire.MilliSatoshi - - // ChannelCommitment is the current up-to-date commitment for the - // target channel. - ChannelCommitment -} +// ChannelSnapshot is a frozen snapshot of the current channel state. +type ChannelSnapshot = cstate.ChannelSnapshot // Snapshot returns a read-only snapshot of the current channel state. This // snapshot includes information concerning the current settled balance within diff --git a/chanstate/snapshot.go b/chanstate/snapshot.go new file mode 100644 index 0000000000..430740beb8 --- /dev/null +++ b/chanstate/snapshot.go @@ -0,0 +1,44 @@ +package chanstate + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnwire" +) + +// ChannelSnapshot is a frozen snapshot of the current channel state. A +// snapshot is detached from the original channel that generated it, providing +// read-only access to the current or prior state of an active channel. +// +// TODO(roasbeef): remove all together? pretty much just commitment +type ChannelSnapshot struct { + // RemoteIdentity is the identity public key of the remote node that we + // are maintaining the open channel with. + RemoteIdentity btcec.PublicKey + + // ChanPoint is the outpoint that created the channel. This output is + // found within the funding transaction and uniquely identified the + // channel on the resident chain. + ChannelPoint wire.OutPoint + + // ChainHash is the genesis hash of the chain that the channel resides + // within. + ChainHash chainhash.Hash + + // Capacity is the total capacity of the channel. + Capacity btcutil.Amount + + // TotalMSatSent is the total number of milli-satoshis we've sent + // within this channel. + TotalMSatSent lnwire.MilliSatoshi + + // TotalMSatReceived is the total number of milli-satoshis we've + // received within this channel. + TotalMSatReceived lnwire.MilliSatoshi + + // ChannelCommitment is the current up-to-date commitment for the + // target channel. + ChannelCommitment +} From fca52a6b710f1f9089bca8438eda14967271abfc Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 12:06:53 -0300 Subject: [PATCH 29/33] chanstate: move taproot channel helpers Move the backend-neutral taproot shachain and verification nonce helpers into chanstate with the thaw-height threshold they support. Leave channeldb aliases for existing callers while OpenChannel and its receiver methods are moved across the package boundary. --- channeldb/channel.go | 77 ++++++++---------------------------------- chanstate/taproot.go | 79 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 63 deletions(-) create mode 100644 chanstate/taproot.go diff --git a/channeldb/channel.go b/channeldb/channel.go index 81a0ac2e42..ec6cfcffb2 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2,7 +2,6 @@ package channeldb import ( "bytes" - "crypto/hmac" "crypto/sha256" "encoding/binary" "errors" @@ -12,7 +11,6 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -32,16 +30,18 @@ import ( ) const ( - // AbsoluteThawHeightThreshold is the threshold at which a thaw height - // begins to be interpreted as an absolute block height, rather than a - // relative one. - AbsoluteThawHeightThreshold uint32 = 500000 - // HTLCBlindingPointTLV is the tlv type used for storing blinding // points with HTLCs. HTLCBlindingPointTLV tlv.Type = 0 ) +const ( + // AbsoluteThawHeightThreshold is the threshold at which a thaw height + // begins to be interpreted as an absolute block height, rather than a + // relative one. + AbsoluteThawHeightThreshold = cstate.AbsoluteThawHeightThreshold +) + var ( // closedChannelBucket stores summarization information concerning // previously open, but now closed channels. @@ -1672,63 +1672,14 @@ func (c *OpenChannel) SecondCommitmentPoint() (*btcec.PublicKey, error) { } var ( - // taprootRevRootKey is the key used to derive the revocation root for - // the taproot nonces. This is done via HMAC of the existing revocation - // root. - taprootRevRootKey = []byte("taproot-rev-root") -) - -// DeriveMusig2Shachain derives a shachain producer for the taproot channel -// from normal shachain revocation root. -func DeriveMusig2Shachain(revRoot shachain.Producer) (shachain.Producer, error) { //nolint:ll - // In order to obtain the revocation root hash to create the taproot - // revocation, we'll encode the producer into a buffer, then use that - // to derive the shachain root needed. - var rootHashBuf bytes.Buffer - if err := revRoot.Encode(&rootHashBuf); err != nil { - return nil, fmt.Errorf("unable to encode producer: %w", err) - } - - revRootHash := chainhash.HashH(rootHashBuf.Bytes()) - - // For taproot channel types, we'll also generate a distinct shachain - // root using the same seed information. We'll use this to generate - // verification nonces for the channel. We'll bind with this a simple - // hmac. - taprootRevHmac := hmac.New(sha256.New, taprootRevRootKey) - if _, err := taprootRevHmac.Write(revRootHash[:]); err != nil { - return nil, err - } - - taprootRevRoot := taprootRevHmac.Sum(nil) + // DeriveMusig2Shachain derives a shachain producer for the taproot + // channel from normal shachain revocation root. + DeriveMusig2Shachain = cstate.DeriveMusig2Shachain - // Once we have the root, we can then generate our shachain producer - // and from that generate the per-commitment point. - return shachain.NewRevocationProducerFromBytes( - taprootRevRoot, - ) -} - -// NewMusigVerificationNonce generates the local or verification nonce for -// another musig2 session. In order to permit our implementation to not have to -// write any secret nonce state to disk, we'll use the _next_ shachain -// pre-image as our primary randomness source. When used to generate the nonce -// again to broadcast our commitment hte current height will be used. -func NewMusigVerificationNonce(pubKey *btcec.PublicKey, targetHeight uint64, - shaGen shachain.Producer) (*musig2.Nonces, error) { - - // Now that we know what height we need, we'll grab the shachain - // pre-image at the target destination. - nextPreimage, err := shaGen.AtIndex(targetHeight) - if err != nil { - return nil, err - } - - shaChainRand := musig2.WithCustomRand(bytes.NewBuffer(nextPreimage[:])) - pubKeyOpt := musig2.WithPublicKey(pubKey) - - return musig2.GenNonces(pubKeyOpt, shaChainRand) -} + // NewMusigVerificationNonce generates the local or verification nonce + // for another musig2 session. + NewMusigVerificationNonce = cstate.NewMusigVerificationNonce +) // ChanSyncMsg returns the ChannelReestablish message that should be sent upon // reconnection with the remote peer that we're maintaining this channel with. diff --git a/chanstate/taproot.go b/chanstate/taproot.go new file mode 100644 index 0000000000..3bb575a080 --- /dev/null +++ b/chanstate/taproot.go @@ -0,0 +1,79 @@ +package chanstate + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/shachain" +) + +const ( + // AbsoluteThawHeightThreshold is the threshold at which a thaw height + // begins to be interpreted as an absolute block height, rather than a + // relative one. + AbsoluteThawHeightThreshold uint32 = 500000 +) + +var ( + // taprootRevRootKey is the key used to derive the revocation root for + // the taproot nonces. This is done via HMAC of the existing revocation + // root. + taprootRevRootKey = []byte("taproot-rev-root") +) + +// DeriveMusig2Shachain derives a shachain producer for the taproot channel +// from normal shachain revocation root. +func DeriveMusig2Shachain(revRoot shachain.Producer) (shachain.Producer, error) { //nolint:ll + // In order to obtain the revocation root hash to create the taproot + // revocation, we'll encode the producer into a buffer, then use that + // to derive the shachain root needed. + var rootHashBuf bytes.Buffer + if err := revRoot.Encode(&rootHashBuf); err != nil { + return nil, fmt.Errorf("unable to encode producer: %w", err) + } + + revRootHash := chainhash.HashH(rootHashBuf.Bytes()) + + // For taproot channel types, we'll also generate a distinct shachain + // root using the same seed information. We'll use this to generate + // verification nonces for the channel. We'll bind with this a simple + // hmac. + taprootRevHmac := hmac.New(sha256.New, taprootRevRootKey) + if _, err := taprootRevHmac.Write(revRootHash[:]); err != nil { + return nil, err + } + + taprootRevRoot := taprootRevHmac.Sum(nil) + + // Once we have the root, we can then generate our shachain producer + // and from that generate the per-commitment point. + return shachain.NewRevocationProducerFromBytes( + taprootRevRoot, + ) +} + +// NewMusigVerificationNonce generates the local or verification nonce for +// another musig2 session. In order to permit our implementation to not have to +// write any secret nonce state to disk, we'll use the _next_ shachain +// pre-image as our primary randomness source. When used to generate the nonce +// again to broadcast our commitment hte current height will be used. +func NewMusigVerificationNonce(pubKey *btcec.PublicKey, targetHeight uint64, + shaGen shachain.Producer) (*musig2.Nonces, error) { + + // Now that we know what height we need, we'll grab the shachain + // pre-image at the target destination. + nextPreimage, err := shaGen.AtIndex(targetHeight) + if err != nil { + return nil, err + } + + shaChainRand := musig2.WithCustomRand(bytes.NewBuffer(nextPreimage[:])) + pubKeyOpt := musig2.WithPublicKey(pubKey) + + return musig2.GenNonces(pubKeyOpt, shaChainRand) +} From ea15a2911e7b4310723d7347b4b3747c09e423d5 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 12:09:55 -0300 Subject: [PATCH 30/33] channeldb: add store status check Add a transitional non-locking status predicate for channeldb store code and use it from KV serialization helpers. This avoids calling an unexported OpenChannel helper from channeldb after the type moves into chanstate. --- channeldb/channel.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index ec6cfcffb2..b7b9fadfb4 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -962,6 +962,17 @@ func (c *OpenChannel) hasChanStatus(status ChannelStatus) bool { return c.chanStatus&status == status } +// HasChanStatusForStore returns true if the internal bitfield channel status +// has the specified status bit set, without taking the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb while OpenChannel moves toward chanstate. Callers +// are responsible for synchronization. Normal callers should use +// HasChanStatus. +func (c *OpenChannel) HasChanStatusForStore(status ChannelStatus) bool { + return c.hasChanStatus(status) +} + // ConfirmedScidForStore returns the in-memory confirmed SCID without taking // the channel mutex. // @@ -4388,7 +4399,7 @@ func fundingTxPresent(channel *OpenChannel) bool { return chanType.IsSingleFunder() && chanType.HasFundingTx() && channel.IsInitiator && - !channel.hasChanStatus(ChanStatusRestored) + !channel.HasChanStatusForStore(ChanStatusRestored) } func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { @@ -4520,7 +4531,7 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, 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.hasChanStatus(ChanStatusRestored) { + if channel.HasChanStatusForStore(ChanStatusRestored) { return nil } @@ -4699,7 +4710,7 @@ func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { // If this is a restored channel, then we don't have any commitments to // read. - if channel.hasChanStatus(ChanStatusRestored) { + if channel.HasChanStatusForStore(ChanStatusRestored) { return nil } From f93d5a966c7196f0fa68cf32e6cdb2f36e06685f Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 12:16:34 -0300 Subject: [PATCH 31/33] chanstate: move open channel type Move OpenChannel and its backend-neutral receiver methods into the chanstate package. channeldb now keeps a compatibility alias while retaining the KV store implementation and serialization helpers. Tests that used private channel status fields now use store-facing accessors. --- channeldb/channel.go | 1231 +----------------------------------- channeldb/channel_test.go | 7 +- channeldb/db.go | 4 +- channeldb/db_test.go | 48 +- chanstate/open_channel.go | 1243 +++++++++++++++++++++++++++++++++++++ 5 files changed, 1278 insertions(+), 1255 deletions(-) create mode 100644 chanstate/open_channel.go diff --git a/channeldb/channel.go b/channeldb/channel.go index b7b9fadfb4..038cf5cafa 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2,16 +2,13 @@ package channeldb import ( "bytes" - "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "net" - "sync" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/walletdb" @@ -19,8 +16,6 @@ import ( "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" "github.com/lightningnetwork/lnd/graph/db/models" - "github.com/lightningnetwork/lnd/htlcswitch/hop" - "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lntypes" @@ -228,6 +223,10 @@ const ( ) type ( + // OpenChannel encapsulates the persistent and dynamic state of an open + // channel with a remote node. + OpenChannel = cstate.OpenChannel + // ChannelCommitment is a snapshot of the commitment state at a // particular point in the commitment chain. ChannelCommitment = cstate.ChannelCommitment @@ -606,410 +605,6 @@ const ( FinalHtlcOffchainBit FinalHtlcByte = 1 << 1 ) -// OpenChannel encapsulates the persistent and dynamic state of an open channel -// with a remote node. An open channel supports several options for on-disk -// serialization depending on the exact context. Full (upon channel creation) -// state commitments, and partial (due to a commitment update) writes are -// supported. Each partial write due to a state update appends the new update -// to an on-disk log, which can then subsequently be queried in order to -// "time-travel" to a prior state. -type OpenChannel struct { - // ChanType denotes which type of channel this is. - ChanType ChannelType - - // ChainHash is a hash which represents the blockchain that this - // channel will be opened within. This value is typically the genesis - // hash. In the case that the original chain went through a contentious - // hard-fork, then this value will be tweaked using the unique fork - // point on each branch. - ChainHash chainhash.Hash - - // FundingOutpoint is the outpoint of the final funding transaction. - // This value uniquely and globally identifies the channel within the - // target blockchain as specified by the chain hash parameter. - FundingOutpoint wire.OutPoint - - // ShortChannelID encodes the exact location in the chain in which the - // channel was initially confirmed. This includes: the block height, - // transaction index, and the output within the target transaction. - // - // If IsZeroConf(), then this will the "base" (very first) ALIAS scid - // and the confirmed SCID will be stored in ConfirmedScid. - ShortChannelID lnwire.ShortChannelID - - // IsPending indicates whether a channel's funding transaction has been - // confirmed. - IsPending bool - - // IsInitiator is a bool which indicates if we were the original - // initiator for the channel. This value may affect how higher levels - // negotiate fees, or close the channel. - IsInitiator bool - - // chanStatus is the current status of this channel. If it is not in - // the state Default, it should not be used for forwarding payments. - chanStatus ChannelStatus - - // FundingBroadcastHeight is the height in which the funding - // transaction was broadcast. This value can be used by higher level - // sub-systems to determine if a channel is stale and/or should have - // been confirmed before a certain height. - FundingBroadcastHeight uint32 - - // ConfirmationHeight records the block height at which the funding - // transaction was first confirmed. - ConfirmationHeight uint32 - - // CloseConfirmationHeight records the block height at which the closing - // transaction was first confirmed. This is used to track remaining - // confirmations until the channel is considered fully closed. It is - // None if the closing transaction has not yet been confirmed, or if - // this data was not available (e.g. channels closed before this - // field was introduced). - CloseConfirmationHeight fn.Option[uint32] - - // NumConfsRequired is the number of confirmations a channel's funding - // transaction must have received in order to be considered available - // for normal transactional use. - NumConfsRequired uint16 - - // ChannelFlags holds the flags that were sent as part of the - // open_channel message. - ChannelFlags lnwire.FundingFlag - - // IdentityPub is the identity public key of the remote node this - // channel has been established with. - IdentityPub *btcec.PublicKey - - // Capacity is the total capacity of this channel. - Capacity btcutil.Amount - - // TotalMSatSent is the total number of milli-satoshis we've sent - // within this channel. - TotalMSatSent lnwire.MilliSatoshi - - // TotalMSatReceived is the total number of milli-satoshis we've - // received within this channel. - TotalMSatReceived lnwire.MilliSatoshi - - // InitialLocalBalance is the balance we have during the channel - // opening. When we are not the initiator, this value represents the - // push amount. - InitialLocalBalance lnwire.MilliSatoshi - - // InitialRemoteBalance is the balance they have during the channel - // opening. - InitialRemoteBalance lnwire.MilliSatoshi - - // LocalChanCfg is the channel configuration for the local node. - LocalChanCfg ChannelConfig - - // RemoteChanCfg is the channel configuration for the remote node. - RemoteChanCfg ChannelConfig - - // LocalCommitment is the current local commitment state for the local - // party. This is stored distinct from the state of the remote party - // as there are certain asymmetric parameters which affect the - // structure of each commitment. - LocalCommitment ChannelCommitment - - // RemoteCommitment is the current remote commitment state for the - // remote party. This is stored distinct from the state of the local - // party as there are certain asymmetric parameters which affect the - // structure of each commitment. - RemoteCommitment ChannelCommitment - - // RemoteCurrentRevocation is the current revocation for their - // commitment transaction. However, since this the derived public key, - // we don't yet have the private key so we aren't yet able to verify - // that it's actually in the hash chain. - RemoteCurrentRevocation *btcec.PublicKey - - // RemoteNextRevocation is the revocation key to be used for the *next* - // commitment transaction we create for the local node. Within the - // specification, this value is referred to as the - // per-commitment-point. - RemoteNextRevocation *btcec.PublicKey - - // RevocationProducer is used to generate the revocation in such a way - // that remote side might store it efficiently and have the ability to - // restore the revocation by index if needed. Current implementation of - // secret producer is shachain producer. - RevocationProducer shachain.Producer - - // RevocationStore is used to efficiently store the revocations for - // previous channels states sent to us by remote side. Current - // implementation of secret store is shachain store. - RevocationStore shachain.Store - - // FundingTxn is the transaction containing this channel's funding - // outpoint. Upon restarts, this txn will be rebroadcast if the channel - // is found to be pending. - // - // NOTE: This value will only be populated for single-funder channels - // for which we are the initiator, and that we also have the funding - // transaction for. One can check this by using the HasFundingTx() - // method on the ChanType field. - FundingTxn *wire.MsgTx - - // LocalShutdownScript is set to a pre-set script if the channel was opened - // by the local node with option_upfront_shutdown_script set. If the option - // was not set, the field is empty. - LocalShutdownScript lnwire.DeliveryAddress - - // RemoteShutdownScript is set to a pre-set script if the channel was opened - // by the remote node with option_upfront_shutdown_script set. If the option - // was not set, the field is empty. - RemoteShutdownScript lnwire.DeliveryAddress - - // ThawHeight is the height when a frozen channel once again becomes a - // normal channel. If this is zero, then there're no restrictions on - // this channel. If the value is lower than 500,000, then it's - // interpreted as a relative height, or an absolute height otherwise. - ThawHeight uint32 - - // LastWasRevoke is a boolean that determines if the last update we sent - // was a revocation (true) or a commitment signature (false). - LastWasRevoke bool - - // RevocationKeyLocator stores the KeyLocator information that we will - // need to derive the shachain root for this channel. This allows us to - // have private key isolation from lnd. - RevocationKeyLocator keychain.KeyLocator - - // confirmedScid is the confirmed ShortChannelID for a zero-conf - // channel. If the channel is unconfirmed, then this will be the - // default ShortChannelID. This is only set for zero-conf channels. - confirmedScid lnwire.ShortChannelID - - // Memo is any arbitrary information we wish to store locally about the - // channel that will be useful to our future selves. - Memo []byte - - // TapscriptRoot is an optional tapscript root used to derive the MuSig2 - // funding output. - TapscriptRoot fn.Option[chainhash.Hash] - - // CustomBlob is an optional blob that can be used to store information - // specific to a custom channel type. This information is only created - // at channel funding time, and after wards is to be considered - // immutable. - CustomBlob fn.Option[tlv.Blob] - - // Db persists channel state through the chanstate Store contract. This - // field intentionally keeps the existing name while the code moves from - // channeldb toward chanstate so call sites can become backend - // independent before the OpenChannel type itself is moved. - Db cstate.Store[*OpenChannel] - - // TODO(roasbeef): just need to store local and remote HTLC's? - - sync.RWMutex -} - -// String returns a string representation of the channel. -func (c *OpenChannel) String() string { - indexStr := "height=%v, local_htlc_index=%v, local_log_index=%v, " + - "remote_htlc_index=%v, remote_log_index=%v" - - commit := c.LocalCommitment - local := fmt.Sprintf(indexStr, commit.CommitHeight, - commit.LocalHtlcIndex, commit.LocalLogIndex, - commit.RemoteHtlcIndex, commit.RemoteLogIndex, - ) - - commit = c.RemoteCommitment - remote := fmt.Sprintf(indexStr, commit.CommitHeight, - commit.LocalHtlcIndex, commit.LocalLogIndex, - commit.RemoteHtlcIndex, commit.RemoteLogIndex, - ) - - return fmt.Sprintf("SCID=%v, status=%v, initiator=%v, pending=%v, "+ - "local commitment has %s, remote commitment has %s", - c.ShortChannelID, c.chanStatus, c.IsInitiator, c.IsPending, - local, remote, - ) -} - -// Initiator returns the ChannelParty that originally opened this channel. -func (c *OpenChannel) Initiator() lntypes.ChannelParty { - c.RLock() - defer c.RUnlock() - - if c.IsInitiator { - return lntypes.Local - } - - return lntypes.Remote -} - -// ShortChanID returns the current ShortChannelID of this channel. -func (c *OpenChannel) ShortChanID() lnwire.ShortChannelID { - c.RLock() - defer c.RUnlock() - - return c.ShortChannelID -} - -// ZeroConfRealScid returns the zero-conf channel's confirmed scid. This should -// only be called if IsZeroConf returns true. -func (c *OpenChannel) ZeroConfRealScid() lnwire.ShortChannelID { - c.RLock() - defer c.RUnlock() - - return c.confirmedScid -} - -// ZeroConfConfirmed returns whether the zero-conf channel has confirmed. This -// should only be called if IsZeroConf returns true. -func (c *OpenChannel) ZeroConfConfirmed() bool { - c.RLock() - defer c.RUnlock() - - return c.confirmedScid != hop.Source -} - -// IsZeroConf returns whether the option_zeroconf channel type was negotiated. -func (c *OpenChannel) IsZeroConf() bool { - c.RLock() - defer c.RUnlock() - - return c.ChanType.HasZeroConf() -} - -// IsOptionScidAlias returns whether the option_scid_alias channel type was -// negotiated. -func (c *OpenChannel) IsOptionScidAlias() bool { - c.RLock() - defer c.RUnlock() - - return c.ChanType.HasScidAliasChan() -} - -// NegotiatedAliasFeature returns whether the option-scid-alias feature bit was -// negotiated. -func (c *OpenChannel) NegotiatedAliasFeature() bool { - c.RLock() - defer c.RUnlock() - - return c.ChanType.HasScidAliasFeature() -} - -// ChanStatus returns the current ChannelStatus of this channel. -func (c *OpenChannel) ChanStatus() ChannelStatus { - c.RLock() - defer c.RUnlock() - - return c.chanStatus -} - -// ChannelStatusForStore returns the in-memory channel status without taking -// the channel mutex. -// -// NOTE: This is a preliminary migration hook for KV-backed store code that -// still lives in channeldb while OpenChannel moves toward chanstate. Callers -// are responsible for synchronization. Normal callers should use ChanStatus. -func (c *OpenChannel) ChannelStatusForStore() ChannelStatus { - return c.chanStatus -} - -// SetChannelStatusForStore updates the in-memory channel status without taking -// the channel mutex. -// -// NOTE: This is a preliminary migration hook for KV-backed store code that -// still lives in channeldb while OpenChannel moves toward chanstate. Callers -// are responsible for synchronization. Normal callers should use -// ApplyChanStatus or ClearChanStatus when the status change must be persisted. -func (c *OpenChannel) SetChannelStatusForStore(status ChannelStatus) { - c.chanStatus = status -} - -// ApplyChanStatus allows the caller to modify the internal channel state in a -// thead-safe manner. -func (c *OpenChannel) ApplyChanStatus(status ChannelStatus) error { - c.Lock() - defer c.Unlock() - - return c.Db.ApplyChannelStatus(c, status) -} - -// ClearChanStatus allows the caller to clear a particular channel status from -// the primary channel status bit field. After this method returns, a call to -// HasChanStatus(status) should return false. -func (c *OpenChannel) ClearChanStatus(status ChannelStatus) error { - c.Lock() - defer c.Unlock() - - return c.Db.ClearChannelStatus(c, status) -} - -// HasChanStatus returns true if the internal bitfield channel status of the -// target channel has the specified status bit set. -func (c *OpenChannel) HasChanStatus(status ChannelStatus) bool { - c.RLock() - defer c.RUnlock() - - return c.hasChanStatus(status) -} - -func (c *OpenChannel) hasChanStatus(status ChannelStatus) bool { - // Special case ChanStatusDefualt since it isn't actually flag, but a - // particular combination (or lack-there-of) of flags. - if status == ChanStatusDefault { - return c.chanStatus == ChanStatusDefault - } - - return c.chanStatus&status == status -} - -// HasChanStatusForStore returns true if the internal bitfield channel status -// has the specified status bit set, without taking the channel mutex. -// -// NOTE: This is a preliminary migration hook for KV-backed store code that -// still lives in channeldb while OpenChannel moves toward chanstate. Callers -// are responsible for synchronization. Normal callers should use -// HasChanStatus. -func (c *OpenChannel) HasChanStatusForStore(status ChannelStatus) bool { - return c.hasChanStatus(status) -} - -// ConfirmedScidForStore returns the in-memory confirmed SCID without taking -// the channel mutex. -// -// NOTE: This is a preliminary migration hook for KV-backed store code that -// still lives in channeldb while OpenChannel moves toward chanstate. Callers -// are responsible for synchronization. Normal callers should use -// ZeroConfRealScid. -func (c *OpenChannel) ConfirmedScidForStore() lnwire.ShortChannelID { - return c.confirmedScid -} - -// SetConfirmedScidForStore updates the in-memory confirmed SCID without taking -// the channel mutex. -// -// NOTE: This is a preliminary migration hook for KV-backed store code that -// still lives in channeldb while OpenChannel moves toward chanstate. Callers -// are responsible for synchronization. -func (c *OpenChannel) SetConfirmedScidForStore(scid lnwire.ShortChannelID) { - c.confirmedScid = scid -} - -// BroadcastHeight returns the height at which the funding tx was broadcast. -func (c *OpenChannel) BroadcastHeight() uint32 { - c.RLock() - defer c.RUnlock() - - return c.FundingBroadcastHeight -} - -// SetBroadcastHeight sets the FundingBroadcastHeight. -func (c *OpenChannel) SetBroadcastHeight(height uint32) { - c.Lock() - defer c.Unlock() - - c.FundingBroadcastHeight = height -} - // amendOpenChannelTlvData updates the channel with the given auxiliary TLV // data. func amendOpenChannelTlvData(channel *OpenChannel, auxData openChannelTlvData) { @@ -1082,15 +677,6 @@ func extractOpenChannelTlvData(channel *OpenChannel) openChannelTlvData { return auxData } -// Refresh updates the in-memory channel state using the latest state observed -// on disk. -func (c *OpenChannel) Refresh() error { - c.Lock() - defer c.Unlock() - - return c.Db.RefreshChannel(c) -} - // RefreshChannel updates the in-memory channel state using the latest state // observed on disk. func (c *ChannelStateDB) RefreshChannel(channel *OpenChannel) error { @@ -1358,21 +944,6 @@ func fullSyncOpenChannel(tx kvdb.RwTx, c *OpenChannel) error { return putOpenChannel(chanBucket, c) } -// MarkConfirmationHeight updates the channel's confirmation height once the -// channel opening transaction receives one confirmation. -func (c *OpenChannel) MarkConfirmationHeight(height uint32) error { - c.Lock() - defer c.Unlock() - - if err := c.Db.MarkChannelConfirmationHeight(c, height); err != nil { - return err - } - - c.ConfirmationHeight = height - - return nil -} - // MarkChannelConfirmationHeight updates the channel's confirmation height once // the channel opening transaction receives one confirmation. func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, @@ -1400,30 +971,6 @@ func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, }, func() {}) } -// ResetCloseConfirmationHeight clears the channel's close confirmation height -// when the spending transaction is reorged out. -func (c *OpenChannel) ResetCloseConfirmationHeight() error { - return c.MarkCloseConfirmationHeight(fn.None[uint32]()) -} - -// MarkCloseConfirmationHeight updates the channel's close confirmation height -// when the closing transaction is first detected in a block (spend height). -func (c *OpenChannel) MarkCloseConfirmationHeight( - height fn.Option[uint32]) error { - - c.Lock() - defer c.Unlock() - - err := c.Db.MarkChannelCloseConfirmationHeight(c, height) - if err != nil { - return err - } - - c.CloseConfirmationHeight = height - - return nil -} - // MarkChannelCloseConfirmationHeight updates the channel's close confirmation // height when the closing transaction is first detected in a block. func (c *ChannelStateDB) MarkChannelCloseConfirmationHeight( @@ -1451,22 +998,6 @@ func (c *ChannelStateDB) MarkChannelCloseConfirmationHeight( }, func() {}) } -// MarkAsOpen marks a channel as fully open given a locator that uniquely -// describes its location within the chain. -func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error { - c.Lock() - defer c.Unlock() - - if err := c.Db.MarkChannelOpen(c, openLoc); err != nil { - return err - } - - c.IsPending = false - c.ShortChannelID = openLoc - - return nil -} - // MarkChannelOpen marks a channel as fully open given a locator that uniquely // describes its location within the chain. func (c *ChannelStateDB) MarkChannelOpen(channel *OpenChannel, @@ -1495,21 +1026,6 @@ func (c *ChannelStateDB) MarkChannelOpen(channel *OpenChannel, }, func() {}) } -// MarkRealScid marks the zero-conf channel's confirmed ShortChannelID. This -// should only be done if IsZeroConf returns true. -func (c *OpenChannel) MarkRealScid(realScid lnwire.ShortChannelID) error { - c.Lock() - defer c.Unlock() - - if err := c.Db.MarkChannelRealScid(c, realScid); err != nil { - return err - } - - c.confirmedScid = realScid - - return nil -} - // MarkChannelRealScid marks the zero-conf channel's confirmed ShortChannelID. func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, realScid lnwire.ShortChannelID) error { @@ -1536,21 +1052,6 @@ func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, }, func() {}) } -// MarkScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in-memory and -// in the database. -func (c *OpenChannel) MarkScidAliasNegotiated() error { - c.Lock() - defer c.Unlock() - - if err := c.Db.MarkChannelScidAliasNegotiated(c); err != nil { - return err - } - - c.ChanType |= ScidAliasFeatureBit - - return nil -} - // MarkChannelScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in the // database. func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( @@ -1578,16 +1079,6 @@ func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( }, func() {}) } -// MarkDataLoss marks sets the channel status to LocalDataLoss and stores the -// passed commitPoint for use to retrieve funds in case the remote force closes -// the channel. -func (c *OpenChannel) MarkDataLoss(commitPoint *btcec.PublicKey) error { - c.Lock() - defer c.Unlock() - - return c.Db.MarkChannelDataLoss(c, commitPoint) -} - // MarkChannelDataLoss marks the channel as local-data-loss and stores the // commit point needed if the remote force closes. func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, @@ -1605,12 +1096,6 @@ func (c *ChannelStateDB) MarkChannelDataLoss(channel *OpenChannel, return c.putChanStatus(channel, ChanStatusLocalDataLoss, putCommitPoint) } -// DataLossCommitPoint retrieves the stored commit point set during -// MarkDataLoss. If not found ErrNoCommitPoint is returned. -func (c *OpenChannel) DataLossCommitPoint() (*btcec.PublicKey, error) { - return c.Db.FetchChannelDataLossCommitPoint(c) -} - // FetchChannelDataLossCommitPoint retrieves the commit point stored when the // channel was marked as local-data-loss. func (c *ChannelStateDB) FetchChannelDataLossCommitPoint( @@ -1651,37 +1136,11 @@ func (c *ChannelStateDB) FetchChannelDataLossCommitPoint( return commitPoint, nil } -// MarkBorked marks the event when the channel as reached an irreconcilable -// state, such as a channel breach or state desynchronization. Borked channels -// should never be added to the switch. -func (c *OpenChannel) MarkBorked() error { - c.Lock() - defer c.Unlock() - - return c.Db.MarkChannelBorked(c) -} - // MarkChannelBorked marks the channel as irreconcilable. func (c *ChannelStateDB) MarkChannelBorked(channel *OpenChannel) error { return c.ApplyChannelStatus(channel, ChanStatusBorked) } -// SecondCommitmentPoint returns the second per-commitment-point for use in the -// channel_ready message. -func (c *OpenChannel) SecondCommitmentPoint() (*btcec.PublicKey, error) { - c.RLock() - defer c.RUnlock() - - // Since we start at commitment height = 0, the second per commitment - // point is actually at the 1st index. - revocation, err := c.RevocationProducer.AtIndex(1) - if err != nil { - return nil, err - } - - return input.ComputeCommitmentPoint(revocation[:]), nil -} - var ( // DeriveMusig2Shachain derives a shachain producer for the taproot // channel from normal shachain revocation root. @@ -1692,145 +1151,6 @@ var ( NewMusigVerificationNonce = cstate.NewMusigVerificationNonce ) -// ChanSyncMsg returns the ChannelReestablish message that should be sent upon -// reconnection with the remote peer that we're maintaining this channel with. -// The information contained within this message is necessary to re-sync our -// commitment chains in the case of a last or only partially processed message. -// When the remote party receives this message one of three things may happen: -// -// 1. We're fully synced and no messages need to be sent. -// 2. We didn't get the last CommitSig message they sent, so they'll re-send -// it. -// 3. We didn't get the last RevokeAndAck message they sent, so they'll -// re-send it. -// -// If this is a restored channel, having status ChanStatusRestored, then we'll -// modify our typical chan sync message to ensure they force close even if -// we're on the very first state. -func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) { - - c.Lock() - defer c.Unlock() - - // The remote commitment height that we'll send in the - // ChannelReestablish message is our current commitment height plus - // one. If the receiver thinks that our commitment height is actually - // *equal* to this value, then they'll re-send the last commitment that - // they sent but we never fully processed. - localHeight := c.LocalCommitment.CommitHeight - nextLocalCommitHeight := localHeight + 1 - - // The second value we'll send is the height of the remote commitment - // from our PoV. If the receiver thinks that their height is actually - // *one plus* this value, then they'll re-send their last revocation. - remoteChainTipHeight := c.RemoteCommitment.CommitHeight - - // If this channel has undergone a commitment update, then in order to - // prove to the remote party our knowledge of their prior commitment - // state, we'll also send over the last commitment secret that the - // remote party sent. - var lastCommitSecret [32]byte - if remoteChainTipHeight != 0 { - remoteSecret, err := c.RevocationStore.LookUp( - remoteChainTipHeight - 1, - ) - if err != nil { - return nil, err - } - lastCommitSecret = [32]byte(*remoteSecret) - } - - // Additionally, we'll send over the current unrevoked commitment on - // our local commitment transaction. - currentCommitSecret, err := c.RevocationProducer.AtIndex( - localHeight, - ) - if err != nil { - return nil, err - } - - // If we've restored this channel, then we'll purposefully give them an - // invalid LocalUnrevokedCommitPoint so they'll force close the channel - // allowing us to sweep our funds. - if c.hasChanStatus(ChanStatusRestored) { - currentCommitSecret[0] ^= 1 - - // If this is a tweakless channel, then we'll purposefully send - // a next local height taht's invalid to trigger a force close - // on their end. We do this as tweakless channels don't require - // that the commitment point is valid, only that it's present. - if c.ChanType.IsTweakless() { - nextLocalCommitHeight = 0 - } - } - - // If this is a taproot channel, then we'll need to generate our next - // verification nonce to send to the remote party. They'll use this to - // sign the next update to our commitment transaction. - var ( - nextTaprootNonce lnwire.OptMusig2NonceTLV - nextLocalNonces lnwire.OptLocalNonces - ) - if c.ChanType.IsTaproot() { - taprootRevProducer, err := DeriveMusig2Shachain( - c.RevocationProducer, - ) - if err != nil { - return nil, err - } - - nextNonce, err := NewMusigVerificationNonce( - c.LocalChanCfg.MultiSigKey.PubKey, - nextLocalCommitHeight, taprootRevProducer, - ) - if err != nil { - return nil, fmt.Errorf("unable to gen next "+ - "nonce: %w", err) - } - - fundingTxid := c.FundingOutpoint.Hash - nonce := nextNonce.PubNonce - - // Final taproot channels use the map-based LocalNonces - // field keyed by funding TXID. Staging channels use the - // legacy single LocalNonce field. - if c.ChanType.IsTaprootFinal() { - noncesMap := make(map[chainhash.Hash]lnwire.Musig2Nonce) - noncesMap[fundingTxid] = nonce - nextLocalNonces = lnwire.SomeLocalNonces( - lnwire.LocalNoncesData{NoncesMap: noncesMap}, - ) - } else { - nextTaprootNonce = lnwire.SomeMusig2Nonce(nonce) - } - } - - return &lnwire.ChannelReestablish{ - ChanID: lnwire.NewChanIDFromOutPoint( - c.FundingOutpoint, - ), - NextLocalCommitHeight: nextLocalCommitHeight, - RemoteCommitTailHeight: remoteChainTipHeight, - LastRemoteCommitSecret: lastCommitSecret, - LocalUnrevokedCommitPoint: input.ComputeCommitmentPoint( - currentCommitSecret[:], - ), - LocalNonce: nextTaprootNonce, - LocalNonces: nextLocalNonces, - }, nil -} - -// MarkShutdownSent serialises and persist the given ShutdownInfo for this -// channel. Persisting this info represents the fact that we have sent the -// Shutdown message to the remote side and hence that we should re-transmit the -// same Shutdown message on re-establish. -func (c *OpenChannel) MarkShutdownSent(info *ShutdownInfo) error { - c.Lock() - defer c.Unlock() - - return c.Db.StoreChannelShutdownInfo(c, info) -} - // StoreChannelShutdownInfo persists the ShutdownInfo for the target channel. func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, info *ShutdownInfo) error { @@ -1854,16 +1174,6 @@ func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, }, func() {}) } -// ShutdownInfo decodes the shutdown info stored for this channel and returns -// the result. If no shutdown info has been persisted for this channel then the -// ErrNoShutdownInfo error is returned. -func (c *OpenChannel) ShutdownInfo() (fn.Option[ShutdownInfo], error) { - c.RLock() - defer c.RUnlock() - - return c.Db.FetchChannelShutdownInfo(c) -} - // FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the target // channel. func (c *ChannelStateDB) FetchChannelShutdownInfo( @@ -1920,18 +1230,6 @@ func isChannelBorked(channel *OpenChannel, chanBucket kvdb.RBucket) ( return diskChannel.ChannelStatusForStore() != ChanStatusDefault, nil } -// MarkCommitmentBroadcasted marks the channel as a commitment transaction has -// been broadcast, either our own or the remote, and we should watch the chain -// for it to confirm before taking any further action. It takes as argument the -// closing tx _we believe_ will appear in the chain. This is only used to -// republish this tx at startup to ensure propagation, and we should still -// handle the case where a different tx actually hits the chain. -func (c *OpenChannel) MarkCommitmentBroadcasted(closeTx *wire.MsgTx, - closer lntypes.ChannelParty) error { - - return c.Db.MarkChannelCommitmentBroadcasted(c, closeTx, closer) -} - // MarkChannelCommitmentBroadcasted marks the channel as having a commitment // transaction broadcast. func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( @@ -1944,19 +1242,6 @@ func (c *ChannelStateDB) MarkChannelCommitmentBroadcasted( ) } -// MarkCoopBroadcasted marks the channel to indicate that a cooperative close -// transaction has been broadcast, either our own or the remote, and that we -// should watch the chain for it to confirm before taking further action. It -// takes as argument a cooperative close tx that could appear on chain, and -// should be rebroadcast upon startup. This is only used to republish and -// ensure propagation, and we should still handle the case where a different tx -// actually hits the chain. -func (c *OpenChannel) MarkCoopBroadcasted(closeTx *wire.MsgTx, - closer lntypes.ChannelParty) error { - - return c.Db.MarkChannelCoopBroadcasted(c, closeTx, closer) -} - // MarkChannelCoopBroadcasted marks the channel as having a cooperative close // transaction broadcast. func (c *ChannelStateDB) MarkChannelCoopBroadcasted(channel *OpenChannel, @@ -2004,12 +1289,6 @@ func (c *ChannelStateDB) markBroadcasted(channel *OpenChannel, return c.putChanStatus(channel, status, putClosingTx) } -// BroadcastedCommitment retrieves the stored unilateral closing tx set during -// MarkCommitmentBroadcasted. If not found ErrNoCloseTx is returned. -func (c *OpenChannel) BroadcastedCommitment() (*wire.MsgTx, error) { - return c.Db.FetchChannelBroadcastedCommitment(c) -} - // FetchChannelBroadcastedCommitment fetches the stored unilateral closing // transaction. func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( @@ -2018,12 +1297,6 @@ func (c *ChannelStateDB) FetchChannelBroadcastedCommitment( return c.getClosingTx(channel, forceCloseTxKey) } -// BroadcastedCooperative retrieves the stored cooperative closing tx set during -// MarkCoopBroadcasted. If not found ErrNoCloseTx is returned. -func (c *OpenChannel) BroadcastedCooperative() (*wire.MsgTx, error) { - return c.Db.FetchChannelBroadcastedCooperative(c) -} - // FetchChannelBroadcastedCooperative fetches the stored cooperative closing // transaction. func (c *ChannelStateDB) FetchChannelBroadcastedCooperative( @@ -2244,24 +1517,6 @@ func fetchOpenChannel(chanBucket kvdb.RBucket, return channel, nil } -// SyncPending writes the contents of the channel to the database while it's in -// the pending (waiting for funding confirmation) state. The IsPending flag -// will be set to true. When the channel's funding transaction is confirmed, -// the channel should be marked as "open" and the IsPending flag set to false. -// Note that this function also creates a LinkNode relationship between this -// newly created channel and a new LinkNode instance. This allows listing all -// channels in the database globally, or according to the LinkNode they were -// created with. -// -// TODO(roasbeef): addr param should eventually be an lnwire.NetAddress type -// that includes service bits. -func (c *OpenChannel) SyncPending(addr net.Addr, pendingHeight uint32) error { - c.Lock() - defer c.Unlock() - - return c.Db.SyncPendingChannel(c, addr, pendingHeight) -} - // SyncPendingChannel writes a pending channel to the store and records the // funding broadcast height. func (c *ChannelStateDB) SyncPendingChannel(channel *OpenChannel, @@ -2309,43 +1564,6 @@ func syncNewChannel(tx kvdb.RwTx, c *OpenChannel, addrs []net.Addr, return putLinkNode(nodeInfoBucket, linkNode) } -// UpdateCommitment updates the local commitment state. It locks in the pending -// local updates that were received by us from the remote party. The commitment -// state completely describes the balance state at this point in the commitment -// chain. In addition to that, it persists all the remote log updates that we -// have acked, but not signed a remote commitment for yet. These need to be -// persisted to be able to produce a valid commit signature if a restart would -// occur. This method its to be called when we revoke our prior commitment -// state. -// -// A map is returned of all the htlc resolutions that were locked in this -// commitment. Keys correspond to htlc indices and values indicate whether the -// htlc was settled or failed. -func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, - unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) { - - c.Lock() - defer c.Unlock() - - // If this is a restored channel, then we want to avoid mutating the - // state as all, as it's impossible to do so in a protocol compliant - // manner. - if c.hasChanStatus(ChanStatusRestored) { - return nil, ErrNoRestoredChannelMutation - } - - finalHtlcs, err := c.Db.UpdateChannelCommitment( - c, newCommitment, unsignedAckedUpdates, - ) - if err != nil { - return nil, err - } - - c.LocalCommitment = *newCommitment - - return finalHtlcs, nil -} - // UpdateChannelCommitment updates the local commitment state. func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, newCommitment *ChannelCommitment, @@ -2527,48 +1745,6 @@ func processFinalHtlc(finalHtlcsBucket walletdb.ReadWriteBucket, upd LogUpdate, return nil } -// ActiveHtlcs returns a slice of HTLC's which are currently active on *both* -// commitment transactions. -func (c *OpenChannel) ActiveHtlcs() []HTLC { - c.RLock() - defer c.RUnlock() - - // We'll only return HTLC's that are locked into *both* commitment - // transactions. So we'll iterate through their set of HTLC's to note - // which ones are present on their commitment. - remoteHtlcs := make(map[[32]byte]struct{}) - for _, htlc := range c.RemoteCommitment.Htlcs { - log.Tracef("RemoteCommitment has htlc: id=%v, update=%v "+ - "incoming=%v", htlc.HtlcIndex, htlc.LogIndex, - htlc.Incoming) - - onionHash := sha256.Sum256(htlc.OnionBlob[:]) - remoteHtlcs[onionHash] = struct{}{} - } - - // Now that we know which HTLC's they have, we'll only mark the HTLC's - // as active if *we* know them as well. - activeHtlcs := make([]HTLC, 0, len(remoteHtlcs)) - for _, htlc := range c.LocalCommitment.Htlcs { - log.Tracef("LocalCommitment has htlc: id=%v, update=%v "+ - "incoming=%v", htlc.HtlcIndex, htlc.LogIndex, - htlc.Incoming) - - onionHash := sha256.Sum256(htlc.OnionBlob[:]) - if _, ok := remoteHtlcs[onionHash]; !ok { - log.Tracef("Skipped htlc due to onion mismatched: "+ - "id=%v, update=%v incoming=%v", - htlc.HtlcIndex, htlc.LogIndex, htlc.Incoming) - - continue - } - - activeHtlcs = append(activeHtlcs, htlc) - } - - return activeHtlcs -} - // 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 @@ -2924,26 +2100,6 @@ func newChannelPackager(channel *OpenChannel) *ChannelPackager { return NewChannelPackager(channel.ShortChannelID) } -// AppendRemoteCommitChain appends a new CommitDiff to the end of the -// commitment chain for the remote party. This method is to be used once we -// have prepared a new commitment state for the remote party, but before we -// transmit it to the remote party. The contents of the argument should be -// sufficient to retransmit the updates and signature needed to reconstruct the -// state in full, in the case that we need to retransmit. -func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error { - c.Lock() - defer c.Unlock() - - // If this is a restored channel, then we want to avoid mutating the - // state at all, as it's impossible to do so in a protocol compliant - // manner. - if c.hasChanStatus(ChanStatusRestored) { - return ErrNoRestoredChannelMutation - } - - return c.Db.AppendRemoteCommitChain(c, diff) -} - // AppendRemoteCommitChain appends a new CommitDiff to the remote party's // commitment chain. func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, @@ -3016,16 +2172,6 @@ func (c *ChannelStateDB) AppendRemoteCommitChain(channel *OpenChannel, }, func() {}) } -// RemoteCommitChainTip returns the "tip" of the current remote commitment -// chain. This value will be non-nil iff, we've created a new commitment for -// the remote party that they haven't yet ACK'd. In this case, their commitment -// chain will have a length of two: their current unrevoked commitment, and -// this new pending commitment. Once they revoked their prior state, we'll swap -// these pointers, causing the tip and the tail to point to the same entry. -func (c *OpenChannel) RemoteCommitChainTip() (*CommitDiff, error) { - return c.Db.RemoteCommitChainTip(c) -} - // RemoteCommitChainTip returns the "tip" of the current remote commitment // chain. func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( @@ -3068,12 +2214,6 @@ func (c *ChannelStateDB) RemoteCommitChainTip(channel *OpenChannel) ( return cd, nil } -// UnsignedAckedUpdates retrieves the persisted unsigned acked remote log -// updates that still need to be signed for. -func (c *OpenChannel) UnsignedAckedUpdates() ([]LogUpdate, error) { - return c.Db.UnsignedAckedUpdates(c) -} - // UnsignedAckedUpdates retrieves the persisted unsigned acked remote log // updates that still need to be signed for. func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( @@ -3111,12 +2251,6 @@ func (c *ChannelStateDB) UnsignedAckedUpdates(channel *OpenChannel) ( return updates, nil } -// RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log -// updates that the remote still needs to sign for. -func (c *OpenChannel) RemoteUnsignedLocalUpdates() ([]LogUpdate, error) { - return c.Db.RemoteUnsignedLocalUpdates(c) -} - // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log // updates that the remote still needs to sign for. func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( @@ -3155,20 +2289,6 @@ func (c *ChannelStateDB) RemoteUnsignedLocalUpdates(channel *OpenChannel) ( return updates, nil } -// InsertNextRevocation inserts the _next_ commitment point (revocation) into -// the database, and also modifies the internal RemoteNextRevocation attribute -// to point to the passed key. This method is to be using during final channel -// set up, _after_ the channel has been fully confirmed. -// -// NOTE: If this method isn't called, then the target channel won't be able to -// propose new states for the commitment state of the remote party. -func (c *OpenChannel) InsertNextRevocation(revKey *btcec.PublicKey) error { - c.Lock() - defer c.Unlock() - - return c.Db.InsertNextRevocation(c, revKey) -} - // InsertNextRevocation inserts the next commitment point into the persisted // channel state. func (c *ChannelStateDB) InsertNextRevocation(channel *OpenChannel, @@ -3194,33 +2314,6 @@ func (c *ChannelStateDB) InsertNextRevocation(channel *OpenChannel, return nil } -// AdvanceCommitChainTail records the new state transition within an on-disk -// append-only log which records all state transitions by the remote peer. In -// the case of an uncooperative broadcast of a prior state by the remote peer, -// this log can be consulted in order to reconstruct the state needed to -// rectify the situation. This method will add the current commitment for the -// remote party to the revocation log, and promote the current pending -// commitment to the current remote commitment. The updates parameter is the -// set of local updates that the peer still needs to send us a signature for. -// We store this set of updates in case we go down. -func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, - updates []LogUpdate, ourOutputIndex, theirOutputIndex uint32) error { - - c.Lock() - defer c.Unlock() - - // If this is a restored channel, then we want to avoid mutating the - // state at all, as it's impossible to do so in a protocol compliant - // manner. - if c.hasChanStatus(ChanStatusRestored) { - return ErrNoRestoredChannelMutation - } - - return c.Db.AdvanceCommitChainTail( - c, fwdPkg, updates, ourOutputIndex, theirOutputIndex, - ) -} - // AdvanceCommitChainTail records the new state transition within the // revocation log and promotes the pending remote commitment to the current // remote commitment. @@ -3400,39 +2493,6 @@ func putFinalHtlc(finalHtlcsBucket kvdb.RwBucket, id uint64, return finalHtlcsBucket.Put(key[:], []byte{byte(finalHtlcByte)}) } -// NextLocalHtlcIndex returns the next unallocated local htlc index. To ensure -// this always returns the next index that has been not been allocated, this -// will first try to examine any pending commitments, before falling back to the -// last locked-in remote commitment. -func (c *OpenChannel) NextLocalHtlcIndex() (uint64, error) { - // First, load the most recent commit diff that we initiated for the - // remote party. If no pending commit is found, this is not treated as - // a critical error, since we can always fall back. - pendingRemoteCommit, err := c.RemoteCommitChainTip() - if err != nil && err != ErrNoPendingCommit { - return 0, err - } - - // If a pending commit was found, its local htlc index will be at least - // as large as the one on our local commitment. - if pendingRemoteCommit != nil { - return pendingRemoteCommit.Commitment.LocalHtlcIndex, nil - } - - // Otherwise, fallback to using the local htlc index of their commitment. - return c.RemoteCommitment.LocalHtlcIndex, nil -} - -// 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 (c *OpenChannel) LoadFwdPkgs() ([]*FwdPkg, error) { - c.RLock() - defer c.RUnlock() - - return c.Db.LoadFwdPkgs(c) -} - // 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. @@ -3453,16 +2513,6 @@ func (c *ChannelStateDB) LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, 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 (c *OpenChannel) AckAddHtlcs(addRefs ...AddRef) error { - c.Lock() - defer c.Unlock() - - return c.Db.AckAddHtlcs(c, addRefs...) -} - // 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. @@ -3474,17 +2524,6 @@ func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, }, 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 (c *OpenChannel) AckSettleFails(settleFailRefs ...SettleFailRef) error { - c.Lock() - defer c.Unlock() - - return c.Db.AckSettleFails(c, settleFailRefs...) -} - // 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 @@ -3499,15 +2538,6 @@ func (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, }, func() {}) } -// SetFwdFilter atomically sets the forwarding filter for the forwarding package -// identified by `height`. -func (c *OpenChannel) SetFwdFilter(height uint64, fwdFilter *PkgFilter) error { - c.Lock() - defer c.Unlock() - - return c.Db.SetFwdFilter(c, height, fwdFilter) -} - // SetFwdFilter atomically sets the forwarding filter for the forwarding package // identified by `height`. func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, @@ -3520,18 +2550,6 @@ func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, }, 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 (c *OpenChannel) RemoveFwdPkgs(heights ...uint64) error { - c.Lock() - defer c.Unlock() - - return c.Db.RemoveFwdPkgs(c, heights...) -} - // 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. @@ -3597,18 +2615,6 @@ func (c *ChannelStateDB) revocationLogTailCommitHeight( return height, 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 (c *OpenChannel) CommitmentHeight() (uint64, error) { - c.RLock() - defer c.RUnlock() - - return c.Db.CommitmentHeight(c) -} - // 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 @@ -3646,20 +2652,6 @@ func (c *ChannelStateDB) CommitmentHeight(channel *OpenChannel) ( 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 (c *OpenChannel) FindPreviousState( - updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { - - c.RLock() - defer c.RUnlock() - - return c.Db.FindPreviousState(c, updateNum) -} - // 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 @@ -3734,25 +2726,6 @@ const ( // was closed. type ChannelCloseSummary = cstate.ChannelCloseSummary -// CloseChannel closes a previously active Lightning channel. Closing a -// channel entails persisting a record of the close while either purging the -// nested per-channel state inline (synchronous backends like bbolt and etcd) -// or skipping the cascading delete on tombstone-enabled backends, where the -// outpoint-index flip to outpointClosed is the authoritative marker. The -// compact summary written to closedChannelBucket and the historical record -// under historicalChannelBucket are populated identically across both paths, -// so historical reads remain uniform regardless of backend. The optional set -// of channel statuses is OR'd into the chanStatus written to the historical -// bucket and is used to record close initiators. -func (c *OpenChannel) CloseChannel(summary *ChannelCloseSummary, - statuses ...ChannelStatus) error { - - c.Lock() - defer c.Unlock() - - return c.Db.CloseChannel(c, summary, statuses...) -} - // CloseChannel closes the supplied channel via the strategy selected at DB // construction. On synchronous backends the channel's nested state — the // revocation log, the per-channel forwarding-package bucket, and the @@ -3977,125 +2950,6 @@ func (c *ChannelStateDB) closeChannelTombstone(channel *OpenChannel, // ChannelSnapshot is a frozen snapshot of the current channel state. type ChannelSnapshot = cstate.ChannelSnapshot -// Snapshot returns a read-only snapshot of the current channel state. This -// snapshot includes information concerning the current settled balance within -// the channel, metadata detailing total flows, and any outstanding HTLCs. -func (c *OpenChannel) Snapshot() *ChannelSnapshot { - c.RLock() - defer c.RUnlock() - - localCommit := c.LocalCommitment - snapshot := &ChannelSnapshot{ - RemoteIdentity: *c.IdentityPub, - ChannelPoint: c.FundingOutpoint, - Capacity: c.Capacity, - TotalMSatSent: c.TotalMSatSent, - TotalMSatReceived: c.TotalMSatReceived, - ChainHash: c.ChainHash, - ChannelCommitment: ChannelCommitment{ - LocalBalance: localCommit.LocalBalance, - RemoteBalance: localCommit.RemoteBalance, - CommitHeight: localCommit.CommitHeight, - CommitFee: localCommit.CommitFee, - }, - } - - localCommit.CustomBlob.WhenSome(func(blob tlv.Blob) { - blobCopy := make([]byte, len(blob)) - copy(blobCopy, blob) - - snapshot.ChannelCommitment.CustomBlob = fn.Some(blobCopy) - }) - - // Copy over the current set of HTLCs to ensure the caller can't mutate - // our internal state. - snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs)) - for i, h := range localCommit.Htlcs { - snapshot.Htlcs[i] = h.Copy() - } - - return snapshot -} - -// Copy returns a deep copy of the channel state. -func (c *OpenChannel) Copy() *OpenChannel { - c.RLock() - defer c.RUnlock() - - clone := &OpenChannel{ - ChanType: c.ChanType, - ChainHash: c.ChainHash, - FundingOutpoint: c.FundingOutpoint, - ShortChannelID: c.ShortChannelID, - IsPending: c.IsPending, - IsInitiator: c.IsInitiator, - chanStatus: c.chanStatus, - FundingBroadcastHeight: c.FundingBroadcastHeight, - ConfirmationHeight: c.ConfirmationHeight, - NumConfsRequired: c.NumConfsRequired, - ChannelFlags: c.ChannelFlags, - IdentityPub: c.IdentityPub, - Capacity: c.Capacity, - TotalMSatSent: c.TotalMSatSent, - TotalMSatReceived: c.TotalMSatReceived, - InitialLocalBalance: c.InitialLocalBalance, - InitialRemoteBalance: c.InitialRemoteBalance, - LocalChanCfg: c.LocalChanCfg, - RemoteChanCfg: c.RemoteChanCfg, - LocalCommitment: c.LocalCommitment.Copy(), - RemoteCommitment: c.RemoteCommitment.Copy(), - RemoteCurrentRevocation: c.RemoteCurrentRevocation, - RemoteNextRevocation: c.RemoteNextRevocation, - RevocationProducer: c.RevocationProducer, - RevocationStore: c.RevocationStore, - ThawHeight: c.ThawHeight, - LastWasRevoke: c.LastWasRevoke, - RevocationKeyLocator: c.RevocationKeyLocator, - confirmedScid: c.confirmedScid, - TapscriptRoot: c.TapscriptRoot, - } - - if c.FundingTxn != nil { - clone.FundingTxn = c.FundingTxn.Copy() - } - - if len(c.LocalShutdownScript) > 0 { - clone.LocalShutdownScript = make( - lnwire.DeliveryAddress, - len(c.LocalShutdownScript), - ) - copy(clone.LocalShutdownScript, c.LocalShutdownScript) - } - if len(c.RemoteShutdownScript) > 0 { - clone.RemoteShutdownScript = make( - lnwire.DeliveryAddress, - len(c.RemoteShutdownScript), - ) - copy(clone.RemoteShutdownScript, c.RemoteShutdownScript) - } - - if len(c.Memo) > 0 { - clone.Memo = make([]byte, len(c.Memo)) - copy(clone.Memo, c.Memo) - } - - c.CustomBlob.WhenSome(func(blob tlv.Blob) { - blobCopy := make([]byte, len(blob)) - copy(blobCopy, blob) - clone.CustomBlob = fn.Some(blobCopy) - }) - - return clone -} - -// 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 (c *OpenChannel) LatestCommitments() (*ChannelCommitment, *ChannelCommitment, error) { - return c.Db.LatestCommitments(c) -} - // 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 @@ -4121,14 +2975,6 @@ func (c *ChannelStateDB) LatestCommitments(channel *OpenChannel) ( 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 (c *OpenChannel) RemoteRevocationStore() (shachain.Store, error) { - return c.Db.RemoteRevocationStore(c) -} - // 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 @@ -4154,75 +3000,6 @@ func (c *ChannelStateDB) RemoteRevocationStore(channel *OpenChannel) ( return channel.RevocationStore, nil } -// AbsoluteThawHeight determines a frozen channel's absolute thaw height. If the -// channel is not frozen, then 0 is returned. -func (c *OpenChannel) AbsoluteThawHeight() (uint32, error) { - // Only frozen channels have a thaw height. - if !c.ChanType.IsFrozen() && !c.ChanType.HasLeaseExpiration() { - return 0, nil - } - - // If the channel has the frozen bit set and it's thaw height is below - // the absolute threshold, then it's interpreted as a relative height to - // the chain's current height. - if c.ChanType.IsFrozen() && c.ThawHeight < AbsoluteThawHeightThreshold { - // We'll only known of the channel's short ID once it's - // confirmed. - if c.IsPending { - return 0, errors.New("cannot use relative thaw " + - "height for unconfirmed channel") - } - - // For non-zero-conf channels, this is the base height to use. - blockHeightBase := c.ShortChannelID.BlockHeight - - // If this is a zero-conf channel, the ShortChannelID will be - // an alias. - if c.IsZeroConf() { - if !c.ZeroConfConfirmed() { - return 0, errors.New("cannot use relative " + - "height for unconfirmed zero-conf " + - "channel") - } - - // Use the confirmed SCID's BlockHeight. - blockHeightBase = c.confirmedScid.BlockHeight - } - - return blockHeightBase + c.ThawHeight, nil - } - - return c.ThawHeight, nil -} - -// DeriveHeightHint derives the block height for the channel opening. -func (c *OpenChannel) DeriveHeightHint() uint32 { - // As a height hint, we'll try to use the opening height, but if the - // channel isn't yet open, then we'll use the height it was broadcast - // at. This may be an unconfirmed zero-conf channel. - heightHint := c.ShortChanID().BlockHeight - if heightHint == 0 { - heightHint = c.BroadcastHeight() - } - - // Since no zero-conf state is stored in a channel backup, the below - // logic will not be triggered for restored, zero-conf channels. Set - // the height hint for zero-conf channels. - if c.IsZeroConf() { - if c.ZeroConfConfirmed() { - // If the zero-conf channel is confirmed, we'll use the - // confirmed SCID's block height. - heightHint = c.ZeroConfRealScid().BlockHeight - } else { - // The zero-conf channel is unconfirmed. We'll need to - // use the FundingBroadcastHeight. - heightHint = c.BroadcastHeight() - } - } - - return heightHint -} - func putChannelCloseSummary(tx kvdb.RwTx, chanID []byte, summary *ChannelCloseSummary, lastChanState *OpenChannel) error { diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 8f155bfc9a..6e1750b3e2 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -1542,7 +1542,7 @@ func TestCloseInitiator(t *testing.T) { if !dbChans[0].HasChanStatus(status) { t.Fatalf("expected channel to have "+ "status: %v, has status: %v", - status, dbChans[0].chanStatus) + status, dbChans[0].ChanStatus()) } } }) @@ -1625,9 +1625,8 @@ func TestHasChanStatus(t *testing.T) { test := test t.Run(test.name, func(t *testing.T) { - c := &OpenChannel{ - chanStatus: test.status, - } + c := &OpenChannel{} + c.SetChannelStatusForStore(test.status) for status, expHas := range test.expHas { has := c.HasChanStatus(status) diff --git a/channeldb/db.go b/channeldb/db.go index 514264dba1..4d563a9a70 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -1694,7 +1694,9 @@ func (c *ChannelStateDB) RestoreChannelShells(channelShells ...*ChannelShell) er // been restored, this will signal to other sub-systems // to not attempt to use the channel as if it was a // regular one. - channel.chanStatus |= ChanStatusRestored + channel.SetChannelStatusForStore( + channel.ChannelStatusForStore() | ChanStatusRestored, + ) // First, we'll attempt to create a new open channel // and link node for this channel. If the channel diff --git a/channeldb/db_test.go b/channeldb/db_test.go index 277820b10c..c973eade72 100644 --- a/channeldb/db_test.go +++ b/channeldb/db_test.go @@ -307,33 +307,35 @@ func genRandomChannelShell() (*ChannelShell, error) { CsvDelay: uint16(rand.Int63()), } + channel := &OpenChannel{ + ChainHash: rev, + FundingOutpoint: chanPoint, + ShortChannelID: lnwire.NewShortChanIDFromInt( + uint64(rand.Int63()), + ), + IdentityPub: pub, + LocalChanCfg: ChannelConfig{ + CommitmentParams: commitParams, + PaymentBasePoint: keychain.KeyDescriptor{ + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(rand.Int63()), + Index: uint32(rand.Int63()), + }, + }, + }, + RemoteCurrentRevocation: pub, + IsPending: false, + RevocationStore: shachain.NewRevocationStore(), + RevocationProducer: shaChainProducer, + } + channel.SetChannelStatusForStore(chanStatus) + return &ChannelShell{ NodeAddrs: []net.Addr{&net.TCPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 18555, }}, - Chan: &OpenChannel{ - chanStatus: chanStatus, - ChainHash: rev, - FundingOutpoint: chanPoint, - ShortChannelID: lnwire.NewShortChanIDFromInt( - uint64(rand.Int63()), - ), - IdentityPub: pub, - LocalChanCfg: ChannelConfig{ - CommitmentParams: commitParams, - PaymentBasePoint: keychain.KeyDescriptor{ - KeyLocator: keychain.KeyLocator{ - Family: keychain.KeyFamily(rand.Int63()), - Index: uint32(rand.Int63()), - }, - }, - }, - RemoteCurrentRevocation: pub, - IsPending: false, - RevocationStore: shachain.NewRevocationStore(), - RevocationProducer: shaChainProducer, - }, + Chan: channel, }, nil } @@ -403,7 +405,7 @@ func TestRestoreChannelShells(t *testing.T) { } if !nodeChans[0].HasChanStatus(ChanStatusRestored) { t.Fatalf("node has wrong status flags: %v", - nodeChans[0].chanStatus) + nodeChans[0].ChanStatus()) } // We should also be able to find the channel if we query for it diff --git a/chanstate/open_channel.go b/chanstate/open_channel.go new file mode 100644 index 0000000000..aadf70d0b5 --- /dev/null +++ b/chanstate/open_channel.go @@ -0,0 +1,1243 @@ +package chanstate + +import ( + "crypto/sha256" + "errors" + "fmt" + "net" + "sync" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/htlcswitch/hop" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" +) + +// OpenChannel encapsulates the persistent and dynamic state of an open channel +// with a remote node. An open channel supports several options for on-disk +// serialization depending on the exact context. Full (upon channel creation) +// state commitments, and partial (due to a commitment update) writes are +// supported. Each partial write due to a state update appends the new update +// to an on-disk log, which can then subsequently be queried in order to +// "time-travel" to a prior state. +type OpenChannel struct { + // ChanType denotes which type of channel this is. + ChanType ChannelType + + // ChainHash is a hash which represents the blockchain that this + // channel will be opened within. This value is typically the genesis + // hash. In the case that the original chain went through a contentious + // hard-fork, then this value will be tweaked using the unique fork + // point on each branch. + ChainHash chainhash.Hash + + // FundingOutpoint is the outpoint of the final funding transaction. + // This value uniquely and globally identifies the channel within the + // target blockchain as specified by the chain hash parameter. + FundingOutpoint wire.OutPoint + + // ShortChannelID encodes the exact location in the chain in which the + // channel was initially confirmed. This includes: the block height, + // transaction index, and the output within the target transaction. + // + // If IsZeroConf(), then this will the "base" (very first) ALIAS scid + // and the confirmed SCID will be stored in ConfirmedScid. + ShortChannelID lnwire.ShortChannelID + + // IsPending indicates whether a channel's funding transaction has been + // confirmed. + IsPending bool + + // IsInitiator is a bool which indicates if we were the original + // initiator for the channel. This value may affect how higher levels + // negotiate fees, or close the channel. + IsInitiator bool + + // chanStatus is the current status of this channel. If it is not in + // the state Default, it should not be used for forwarding payments. + chanStatus ChannelStatus + + // FundingBroadcastHeight is the height in which the funding + // transaction was broadcast. This value can be used by higher level + // sub-systems to determine if a channel is stale and/or should have + // been confirmed before a certain height. + FundingBroadcastHeight uint32 + + // ConfirmationHeight records the block height at which the funding + // transaction was first confirmed. + ConfirmationHeight uint32 + + // CloseConfirmationHeight records the block height at which the closing + // transaction was first confirmed. This is used to track remaining + // confirmations until the channel is considered fully closed. It is + // None if the closing transaction has not yet been confirmed, or if + // this data was not available (e.g. channels closed before this + // field was introduced). + CloseConfirmationHeight fn.Option[uint32] + + // NumConfsRequired is the number of confirmations a channel's funding + // transaction must have received in order to be considered available + // for normal transactional use. + NumConfsRequired uint16 + + // ChannelFlags holds the flags that were sent as part of the + // open_channel message. + ChannelFlags lnwire.FundingFlag + + // IdentityPub is the identity public key of the remote node this + // channel has been established with. + IdentityPub *btcec.PublicKey + + // Capacity is the total capacity of this channel. + Capacity btcutil.Amount + + // TotalMSatSent is the total number of milli-satoshis we've sent + // within this channel. + TotalMSatSent lnwire.MilliSatoshi + + // TotalMSatReceived is the total number of milli-satoshis we've + // received within this channel. + TotalMSatReceived lnwire.MilliSatoshi + + // InitialLocalBalance is the balance we have during the channel + // opening. When we are not the initiator, this value represents the + // push amount. + InitialLocalBalance lnwire.MilliSatoshi + + // InitialRemoteBalance is the balance they have during the channel + // opening. + InitialRemoteBalance lnwire.MilliSatoshi + + // LocalChanCfg is the channel configuration for the local node. + LocalChanCfg ChannelConfig + + // RemoteChanCfg is the channel configuration for the remote node. + RemoteChanCfg ChannelConfig + + // LocalCommitment is the current local commitment state for the local + // party. This is stored distinct from the state of the remote party + // as there are certain asymmetric parameters which affect the + // structure of each commitment. + LocalCommitment ChannelCommitment + + // RemoteCommitment is the current remote commitment state for the + // remote party. This is stored distinct from the state of the local + // party as there are certain asymmetric parameters which affect the + // structure of each commitment. + RemoteCommitment ChannelCommitment + + // RemoteCurrentRevocation is the current revocation for their + // commitment transaction. However, since this the derived public key, + // we don't yet have the private key so we aren't yet able to verify + // that it's actually in the hash chain. + RemoteCurrentRevocation *btcec.PublicKey + + // RemoteNextRevocation is the revocation key to be used for the *next* + // commitment transaction we create for the local node. Within the + // specification, this value is referred to as the + // per-commitment-point. + RemoteNextRevocation *btcec.PublicKey + + // RevocationProducer is used to generate the revocation in such a way + // that remote side might store it efficiently and have the ability to + // restore the revocation by index if needed. Current implementation of + // secret producer is shachain producer. + RevocationProducer shachain.Producer + + // RevocationStore is used to efficiently store the revocations for + // previous channels states sent to us by remote side. Current + // implementation of secret store is shachain store. + RevocationStore shachain.Store + + // FundingTxn is the transaction containing this channel's funding + // outpoint. Upon restarts, this txn will be rebroadcast if the channel + // is found to be pending. + // + // NOTE: This value will only be populated for single-funder channels + // for which we are the initiator, and that we also have the funding + // transaction for. One can check this by using the HasFundingTx() + // method on the ChanType field. + FundingTxn *wire.MsgTx + + // LocalShutdownScript is set to a pre-set script if the channel was opened + // by the local node with option_upfront_shutdown_script set. If the option + // was not set, the field is empty. + LocalShutdownScript lnwire.DeliveryAddress + + // RemoteShutdownScript is set to a pre-set script if the channel was opened + // by the remote node with option_upfront_shutdown_script set. If the option + // was not set, the field is empty. + RemoteShutdownScript lnwire.DeliveryAddress + + // ThawHeight is the height when a frozen channel once again becomes a + // normal channel. If this is zero, then there're no restrictions on + // this channel. If the value is lower than 500,000, then it's + // interpreted as a relative height, or an absolute height otherwise. + ThawHeight uint32 + + // LastWasRevoke is a boolean that determines if the last update we sent + // was a revocation (true) or a commitment signature (false). + LastWasRevoke bool + + // RevocationKeyLocator stores the KeyLocator information that we will + // need to derive the shachain root for this channel. This allows us to + // have private key isolation from lnd. + RevocationKeyLocator keychain.KeyLocator + + // confirmedScid is the confirmed ShortChannelID for a zero-conf + // channel. If the channel is unconfirmed, then this will be the + // default ShortChannelID. This is only set for zero-conf channels. + confirmedScid lnwire.ShortChannelID + + // Memo is any arbitrary information we wish to store locally about the + // channel that will be useful to our future selves. + Memo []byte + + // TapscriptRoot is an optional tapscript root used to derive the MuSig2 + // funding output. + TapscriptRoot fn.Option[chainhash.Hash] + + // CustomBlob is an optional blob that can be used to store information + // specific to a custom channel type. This information is only created + // at channel funding time, and after wards is to be considered + // immutable. + CustomBlob fn.Option[tlv.Blob] + + // Db persists channel state through the Store contract. This field + // intentionally keeps the existing name while callers still construct + // channels through the channeldb compatibility alias. The store + // interface keeps receiver methods backend independent while the KV + // implementation remains in channeldb. + Db Store[*OpenChannel] + + // TODO(roasbeef): just need to store local and remote HTLC's? + + sync.RWMutex +} + +// String returns a string representation of the channel. +func (c *OpenChannel) String() string { + indexStr := "height=%v, local_htlc_index=%v, local_log_index=%v, " + + "remote_htlc_index=%v, remote_log_index=%v" + + commit := c.LocalCommitment + local := fmt.Sprintf(indexStr, commit.CommitHeight, + commit.LocalHtlcIndex, commit.LocalLogIndex, + commit.RemoteHtlcIndex, commit.RemoteLogIndex, + ) + + commit = c.RemoteCommitment + remote := fmt.Sprintf(indexStr, commit.CommitHeight, + commit.LocalHtlcIndex, commit.LocalLogIndex, + commit.RemoteHtlcIndex, commit.RemoteLogIndex, + ) + + return fmt.Sprintf("SCID=%v, status=%v, initiator=%v, pending=%v, "+ + "local commitment has %s, remote commitment has %s", + c.ShortChannelID, c.chanStatus, c.IsInitiator, c.IsPending, + local, remote, + ) +} + +// Initiator returns the ChannelParty that originally opened this channel. +func (c *OpenChannel) Initiator() lntypes.ChannelParty { + c.RLock() + defer c.RUnlock() + + if c.IsInitiator { + return lntypes.Local + } + + return lntypes.Remote +} + +// ShortChanID returns the current ShortChannelID of this channel. +func (c *OpenChannel) ShortChanID() lnwire.ShortChannelID { + c.RLock() + defer c.RUnlock() + + return c.ShortChannelID +} + +// ZeroConfRealScid returns the zero-conf channel's confirmed scid. This should +// only be called if IsZeroConf returns true. +func (c *OpenChannel) ZeroConfRealScid() lnwire.ShortChannelID { + c.RLock() + defer c.RUnlock() + + return c.confirmedScid +} + +// ZeroConfConfirmed returns whether the zero-conf channel has confirmed. This +// should only be called if IsZeroConf returns true. +func (c *OpenChannel) ZeroConfConfirmed() bool { + c.RLock() + defer c.RUnlock() + + return c.confirmedScid != hop.Source +} + +// IsZeroConf returns whether the option_zeroconf channel type was negotiated. +func (c *OpenChannel) IsZeroConf() bool { + c.RLock() + defer c.RUnlock() + + return c.ChanType.HasZeroConf() +} + +// IsOptionScidAlias returns whether the option_scid_alias channel type was +// negotiated. +func (c *OpenChannel) IsOptionScidAlias() bool { + c.RLock() + defer c.RUnlock() + + return c.ChanType.HasScidAliasChan() +} + +// NegotiatedAliasFeature returns whether the option-scid-alias feature bit was +// negotiated. +func (c *OpenChannel) NegotiatedAliasFeature() bool { + c.RLock() + defer c.RUnlock() + + return c.ChanType.HasScidAliasFeature() +} + +// ChanStatus returns the current ChannelStatus of this channel. +func (c *OpenChannel) ChanStatus() ChannelStatus { + c.RLock() + defer c.RUnlock() + + return c.chanStatus +} + +// ChannelStatusForStore returns the in-memory channel status without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb during this refactor. Callers are responsible for +// synchronization. Normal callers should use ChanStatus. +func (c *OpenChannel) ChannelStatusForStore() ChannelStatus { + return c.chanStatus +} + +// SetChannelStatusForStore updates the in-memory channel status without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb during this refactor. Callers are responsible for +// synchronization. Normal callers should use ApplyChanStatus or +// ClearChanStatus when the status change must be persisted. +func (c *OpenChannel) SetChannelStatusForStore(status ChannelStatus) { + c.chanStatus = status +} + +// ApplyChanStatus allows the caller to modify the internal channel state in a +// thead-safe manner. +func (c *OpenChannel) ApplyChanStatus(status ChannelStatus) error { + c.Lock() + defer c.Unlock() + + return c.Db.ApplyChannelStatus(c, status) +} + +// ClearChanStatus allows the caller to clear a particular channel status from +// the primary channel status bit field. After this method returns, a call to +// HasChanStatus(status) should return false. +func (c *OpenChannel) ClearChanStatus(status ChannelStatus) error { + c.Lock() + defer c.Unlock() + + return c.Db.ClearChannelStatus(c, status) +} + +// HasChanStatus returns true if the internal bitfield channel status of the +// target channel has the specified status bit set. +func (c *OpenChannel) HasChanStatus(status ChannelStatus) bool { + c.RLock() + defer c.RUnlock() + + return c.hasChanStatus(status) +} + +func (c *OpenChannel) hasChanStatus(status ChannelStatus) bool { + // Special case ChanStatusDefualt since it isn't actually flag, but a + // particular combination (or lack-there-of) of flags. + if status == ChanStatusDefault { + return c.chanStatus == ChanStatusDefault + } + + return c.chanStatus&status == status +} + +// HasChanStatusForStore returns true if the internal bitfield channel status +// has the specified status bit set, without taking the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb during this refactor. Callers are responsible for +// synchronization. Normal callers should use HasChanStatus. +func (c *OpenChannel) HasChanStatusForStore(status ChannelStatus) bool { + return c.hasChanStatus(status) +} + +// ConfirmedScidForStore returns the in-memory confirmed SCID without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb during this refactor. Callers are responsible for +// synchronization. Normal callers should use ZeroConfRealScid. +func (c *OpenChannel) ConfirmedScidForStore() lnwire.ShortChannelID { + return c.confirmedScid +} + +// SetConfirmedScidForStore updates the in-memory confirmed SCID without taking +// the channel mutex. +// +// NOTE: This is a preliminary migration hook for KV-backed store code that +// still lives in channeldb during this refactor. Callers are responsible for +// synchronization. +func (c *OpenChannel) SetConfirmedScidForStore(scid lnwire.ShortChannelID) { + c.confirmedScid = scid +} + +// BroadcastHeight returns the height at which the funding tx was broadcast. +func (c *OpenChannel) BroadcastHeight() uint32 { + c.RLock() + defer c.RUnlock() + + return c.FundingBroadcastHeight +} + +// SetBroadcastHeight sets the FundingBroadcastHeight. +func (c *OpenChannel) SetBroadcastHeight(height uint32) { + c.Lock() + defer c.Unlock() + + c.FundingBroadcastHeight = height +} + +// Refresh updates the in-memory channel state using the latest state observed +// on disk. +func (c *OpenChannel) Refresh() error { + c.Lock() + defer c.Unlock() + + return c.Db.RefreshChannel(c) +} + +// MarkConfirmationHeight updates the channel's confirmation height once the +// channel opening transaction receives one confirmation. +func (c *OpenChannel) MarkConfirmationHeight(height uint32) error { + c.Lock() + defer c.Unlock() + + if err := c.Db.MarkChannelConfirmationHeight(c, height); err != nil { + return err + } + + c.ConfirmationHeight = height + + return nil +} + +// ResetCloseConfirmationHeight clears the channel's close confirmation height +// when the spending transaction is reorged out. +func (c *OpenChannel) ResetCloseConfirmationHeight() error { + return c.MarkCloseConfirmationHeight(fn.None[uint32]()) +} + +// MarkCloseConfirmationHeight updates the channel's close confirmation height +// when the closing transaction is first detected in a block (spend height). +func (c *OpenChannel) MarkCloseConfirmationHeight( + height fn.Option[uint32]) error { + + c.Lock() + defer c.Unlock() + + err := c.Db.MarkChannelCloseConfirmationHeight(c, height) + if err != nil { + return err + } + + c.CloseConfirmationHeight = height + + return nil +} + +// MarkAsOpen marks a channel as fully open given a locator that uniquely +// describes its location within the chain. +func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error { + c.Lock() + defer c.Unlock() + + if err := c.Db.MarkChannelOpen(c, openLoc); err != nil { + return err + } + + c.IsPending = false + c.ShortChannelID = openLoc + + return nil +} + +// MarkRealScid marks the zero-conf channel's confirmed ShortChannelID. This +// should only be done if IsZeroConf returns true. +func (c *OpenChannel) MarkRealScid(realScid lnwire.ShortChannelID) error { + c.Lock() + defer c.Unlock() + + if err := c.Db.MarkChannelRealScid(c, realScid); err != nil { + return err + } + + c.confirmedScid = realScid + + return nil +} + +// MarkScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in-memory and +// in the database. +func (c *OpenChannel) MarkScidAliasNegotiated() error { + c.Lock() + defer c.Unlock() + + if err := c.Db.MarkChannelScidAliasNegotiated(c); err != nil { + return err + } + + c.ChanType |= ScidAliasFeatureBit + + return nil +} + +// MarkDataLoss marks sets the channel status to LocalDataLoss and stores the +// passed commitPoint for use to retrieve funds in case the remote force closes +// the channel. +func (c *OpenChannel) MarkDataLoss(commitPoint *btcec.PublicKey) error { + c.Lock() + defer c.Unlock() + + return c.Db.MarkChannelDataLoss(c, commitPoint) +} + +// DataLossCommitPoint retrieves the stored commit point set during +// MarkDataLoss. If not found ErrNoCommitPoint is returned. +func (c *OpenChannel) DataLossCommitPoint() (*btcec.PublicKey, error) { + return c.Db.FetchChannelDataLossCommitPoint(c) +} + +// MarkBorked marks the event when the channel as reached an irreconcilable +// state, such as a channel breach or state desynchronization. Borked channels +// should never be added to the switch. +func (c *OpenChannel) MarkBorked() error { + c.Lock() + defer c.Unlock() + + return c.Db.MarkChannelBorked(c) +} + +// SecondCommitmentPoint returns the second per-commitment-point for use in the +// channel_ready message. +func (c *OpenChannel) SecondCommitmentPoint() (*btcec.PublicKey, error) { + c.RLock() + defer c.RUnlock() + + // Since we start at commitment height = 0, the second per commitment + // point is actually at the 1st index. + revocation, err := c.RevocationProducer.AtIndex(1) + if err != nil { + return nil, err + } + + return input.ComputeCommitmentPoint(revocation[:]), nil +} + +// ChanSyncMsg returns the ChannelReestablish message that should be sent upon +// reconnection with the remote peer that we're maintaining this channel with. +// The information contained within this message is necessary to re-sync our +// commitment chains in the case of a last or only partially processed message. +// When the remote party receives this message one of three things may happen: +// +// 1. We're fully synced and no messages need to be sent. +// 2. We didn't get the last CommitSig message they sent, so they'll re-send +// it. +// 3. We didn't get the last RevokeAndAck message they sent, so they'll +// re-send it. +// +// If this is a restored channel, having status ChanStatusRestored, then we'll +// modify our typical chan sync message to ensure they force close even if +// we're on the very first state. +func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) { + + c.Lock() + defer c.Unlock() + + // The remote commitment height that we'll send in the + // ChannelReestablish message is our current commitment height plus + // one. If the receiver thinks that our commitment height is actually + // *equal* to this value, then they'll re-send the last commitment that + // they sent but we never fully processed. + localHeight := c.LocalCommitment.CommitHeight + nextLocalCommitHeight := localHeight + 1 + + // The second value we'll send is the height of the remote commitment + // from our PoV. If the receiver thinks that their height is actually + // *one plus* this value, then they'll re-send their last revocation. + remoteChainTipHeight := c.RemoteCommitment.CommitHeight + + // If this channel has undergone a commitment update, then in order to + // prove to the remote party our knowledge of their prior commitment + // state, we'll also send over the last commitment secret that the + // remote party sent. + var lastCommitSecret [32]byte + if remoteChainTipHeight != 0 { + remoteSecret, err := c.RevocationStore.LookUp( + remoteChainTipHeight - 1, + ) + if err != nil { + return nil, err + } + lastCommitSecret = [32]byte(*remoteSecret) + } + + // Additionally, we'll send over the current unrevoked commitment on + // our local commitment transaction. + currentCommitSecret, err := c.RevocationProducer.AtIndex( + localHeight, + ) + if err != nil { + return nil, err + } + + // If we've restored this channel, then we'll purposefully give them an + // invalid LocalUnrevokedCommitPoint so they'll force close the channel + // allowing us to sweep our funds. + if c.hasChanStatus(ChanStatusRestored) { + currentCommitSecret[0] ^= 1 + + // If this is a tweakless channel, then we'll purposefully send + // a next local height taht's invalid to trigger a force close + // on their end. We do this as tweakless channels don't require + // that the commitment point is valid, only that it's present. + if c.ChanType.IsTweakless() { + nextLocalCommitHeight = 0 + } + } + + // If this is a taproot channel, then we'll need to generate our next + // verification nonce to send to the remote party. They'll use this to + // sign the next update to our commitment transaction. + var ( + nextTaprootNonce lnwire.OptMusig2NonceTLV + nextLocalNonces lnwire.OptLocalNonces + ) + if c.ChanType.IsTaproot() { + taprootRevProducer, err := DeriveMusig2Shachain( + c.RevocationProducer, + ) + if err != nil { + return nil, err + } + + nextNonce, err := NewMusigVerificationNonce( + c.LocalChanCfg.MultiSigKey.PubKey, + nextLocalCommitHeight, taprootRevProducer, + ) + if err != nil { + return nil, fmt.Errorf("unable to gen next "+ + "nonce: %w", err) + } + + fundingTxid := c.FundingOutpoint.Hash + nonce := nextNonce.PubNonce + + // Final taproot channels use the map-based LocalNonces + // field keyed by funding TXID. Staging channels use the + // legacy single LocalNonce field. + if c.ChanType.IsTaprootFinal() { + noncesMap := make(map[chainhash.Hash]lnwire.Musig2Nonce) + noncesMap[fundingTxid] = nonce + nextLocalNonces = lnwire.SomeLocalNonces( + lnwire.LocalNoncesData{NoncesMap: noncesMap}, + ) + } else { + nextTaprootNonce = lnwire.SomeMusig2Nonce(nonce) + } + } + + return &lnwire.ChannelReestablish{ + ChanID: lnwire.NewChanIDFromOutPoint( + c.FundingOutpoint, + ), + NextLocalCommitHeight: nextLocalCommitHeight, + RemoteCommitTailHeight: remoteChainTipHeight, + LastRemoteCommitSecret: lastCommitSecret, + LocalUnrevokedCommitPoint: input.ComputeCommitmentPoint( + currentCommitSecret[:], + ), + LocalNonce: nextTaprootNonce, + LocalNonces: nextLocalNonces, + }, nil +} + +// MarkShutdownSent serialises and persist the given ShutdownInfo for this +// channel. Persisting this info represents the fact that we have sent the +// Shutdown message to the remote side and hence that we should re-transmit the +// same Shutdown message on re-establish. +func (c *OpenChannel) MarkShutdownSent(info *ShutdownInfo) error { + c.Lock() + defer c.Unlock() + + return c.Db.StoreChannelShutdownInfo(c, info) +} + +// ShutdownInfo decodes the shutdown info stored for this channel and returns +// the result. If no shutdown info has been persisted for this channel then the +// ErrNoShutdownInfo error is returned. +func (c *OpenChannel) ShutdownInfo() (fn.Option[ShutdownInfo], error) { + c.RLock() + defer c.RUnlock() + + return c.Db.FetchChannelShutdownInfo(c) +} + +// MarkCommitmentBroadcasted marks the channel as a commitment transaction has +// been broadcast, either our own or the remote, and we should watch the chain +// for it to confirm before taking any further action. It takes as argument the +// closing tx _we believe_ will appear in the chain. This is only used to +// republish this tx at startup to ensure propagation, and we should still +// handle the case where a different tx actually hits the chain. +func (c *OpenChannel) MarkCommitmentBroadcasted(closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error { + + return c.Db.MarkChannelCommitmentBroadcasted(c, closeTx, closer) +} + +// MarkCoopBroadcasted marks the channel to indicate that a cooperative close +// transaction has been broadcast, either our own or the remote, and that we +// should watch the chain for it to confirm before taking further action. It +// takes as argument a cooperative close tx that could appear on chain, and +// should be rebroadcast upon startup. This is only used to republish and +// ensure propagation, and we should still handle the case where a different tx +// actually hits the chain. +func (c *OpenChannel) MarkCoopBroadcasted(closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error { + + return c.Db.MarkChannelCoopBroadcasted(c, closeTx, closer) +} + +// BroadcastedCommitment retrieves the stored unilateral closing tx set during +// MarkCommitmentBroadcasted. If not found ErrNoCloseTx is returned. +func (c *OpenChannel) BroadcastedCommitment() (*wire.MsgTx, error) { + return c.Db.FetchChannelBroadcastedCommitment(c) +} + +// BroadcastedCooperative retrieves the stored cooperative closing tx set during +// MarkCoopBroadcasted. If not found ErrNoCloseTx is returned. +func (c *OpenChannel) BroadcastedCooperative() (*wire.MsgTx, error) { + return c.Db.FetchChannelBroadcastedCooperative(c) +} + +// SyncPending writes the contents of the channel to the database while it's in +// the pending (waiting for funding confirmation) state. The IsPending flag +// will be set to true. When the channel's funding transaction is confirmed, +// the channel should be marked as "open" and the IsPending flag set to false. +// Note that this function also creates a LinkNode relationship between this +// newly created channel and a new LinkNode instance. This allows listing all +// channels in the database globally, or according to the LinkNode they were +// created with. +// +// TODO(roasbeef): addr param should eventually be an lnwire.NetAddress type +// that includes service bits. +func (c *OpenChannel) SyncPending(addr net.Addr, pendingHeight uint32) error { + c.Lock() + defer c.Unlock() + + return c.Db.SyncPendingChannel(c, addr, pendingHeight) +} + +// UpdateCommitment updates the local commitment state. It locks in the pending +// local updates that were received by us from the remote party. The commitment +// state completely describes the balance state at this point in the commitment +// chain. In addition to that, it persists all the remote log updates that we +// have acked, but not signed a remote commitment for yet. These need to be +// persisted to be able to produce a valid commit signature if a restart would +// occur. This method its to be called when we revoke our prior commitment +// state. +// +// A map is returned of all the htlc resolutions that were locked in this +// commitment. Keys correspond to htlc indices and values indicate whether the +// htlc was settled or failed. +func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, + unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) { + + c.Lock() + defer c.Unlock() + + // If this is a restored channel, then we want to avoid mutating the + // state as all, as it's impossible to do so in a protocol compliant + // manner. + if c.hasChanStatus(ChanStatusRestored) { + return nil, ErrNoRestoredChannelMutation + } + + finalHtlcs, err := c.Db.UpdateChannelCommitment( + c, newCommitment, unsignedAckedUpdates, + ) + if err != nil { + return nil, err + } + + c.LocalCommitment = *newCommitment + + return finalHtlcs, nil +} + +// ActiveHtlcs returns a slice of HTLC's which are currently active on *both* +// commitment transactions. +func (c *OpenChannel) ActiveHtlcs() []HTLC { + c.RLock() + defer c.RUnlock() + + // We'll only return HTLC's that are locked into *both* commitment + // transactions. So we'll iterate through their set of HTLC's to note + // which ones are present on their commitment. + remoteHtlcs := make(map[[32]byte]struct{}) + for _, htlc := range c.RemoteCommitment.Htlcs { + log.Tracef("RemoteCommitment has htlc: id=%v, update=%v "+ + "incoming=%v", htlc.HtlcIndex, htlc.LogIndex, + htlc.Incoming) + + onionHash := sha256.Sum256(htlc.OnionBlob[:]) + remoteHtlcs[onionHash] = struct{}{} + } + + // Now that we know which HTLC's they have, we'll only mark the HTLC's + // as active if *we* know them as well. + activeHtlcs := make([]HTLC, 0, len(remoteHtlcs)) + for _, htlc := range c.LocalCommitment.Htlcs { + log.Tracef("LocalCommitment has htlc: id=%v, update=%v "+ + "incoming=%v", htlc.HtlcIndex, htlc.LogIndex, + htlc.Incoming) + + onionHash := sha256.Sum256(htlc.OnionBlob[:]) + if _, ok := remoteHtlcs[onionHash]; !ok { + log.Tracef("Skipped htlc due to onion mismatched: "+ + "id=%v, update=%v incoming=%v", + htlc.HtlcIndex, htlc.LogIndex, htlc.Incoming) + + continue + } + + activeHtlcs = append(activeHtlcs, htlc) + } + + return activeHtlcs +} + +// AppendRemoteCommitChain appends a new CommitDiff to the end of the +// commitment chain for the remote party. This method is to be used once we +// have prepared a new commitment state for the remote party, but before we +// transmit it to the remote party. The contents of the argument should be +// sufficient to retransmit the updates and signature needed to reconstruct the +// state in full, in the case that we need to retransmit. +func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error { + c.Lock() + defer c.Unlock() + + // If this is a restored channel, then we want to avoid mutating the + // state at all, as it's impossible to do so in a protocol compliant + // manner. + if c.hasChanStatus(ChanStatusRestored) { + return ErrNoRestoredChannelMutation + } + + return c.Db.AppendRemoteCommitChain(c, diff) +} + +// RemoteCommitChainTip returns the "tip" of the current remote commitment +// chain. This value will be non-nil iff, we've created a new commitment for +// the remote party that they haven't yet ACK'd. In this case, their commitment +// chain will have a length of two: their current unrevoked commitment, and +// this new pending commitment. Once they revoked their prior state, we'll swap +// these pointers, causing the tip and the tail to point to the same entry. +func (c *OpenChannel) RemoteCommitChainTip() (*CommitDiff, error) { + return c.Db.RemoteCommitChainTip(c) +} + +// UnsignedAckedUpdates retrieves the persisted unsigned acked remote log +// updates that still need to be signed for. +func (c *OpenChannel) UnsignedAckedUpdates() ([]LogUpdate, error) { + return c.Db.UnsignedAckedUpdates(c) +} + +// RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local log +// updates that the remote still needs to sign for. +func (c *OpenChannel) RemoteUnsignedLocalUpdates() ([]LogUpdate, error) { + return c.Db.RemoteUnsignedLocalUpdates(c) +} + +// InsertNextRevocation inserts the _next_ commitment point (revocation) into +// the database, and also modifies the internal RemoteNextRevocation attribute +// to point to the passed key. This method is to be using during final channel +// set up, _after_ the channel has been fully confirmed. +// +// NOTE: If this method isn't called, then the target channel won't be able to +// propose new states for the commitment state of the remote party. +func (c *OpenChannel) InsertNextRevocation(revKey *btcec.PublicKey) error { + c.Lock() + defer c.Unlock() + + return c.Db.InsertNextRevocation(c, revKey) +} + +// AdvanceCommitChainTail records the new state transition within an on-disk +// append-only log which records all state transitions by the remote peer. In +// the case of an uncooperative broadcast of a prior state by the remote peer, +// this log can be consulted in order to reconstruct the state needed to +// rectify the situation. This method will add the current commitment for the +// remote party to the revocation log, and promote the current pending +// commitment to the current remote commitment. The updates parameter is the +// set of local updates that the peer still needs to send us a signature for. +// We store this set of updates in case we go down. +func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, + updates []LogUpdate, ourOutputIndex, theirOutputIndex uint32) error { + + c.Lock() + defer c.Unlock() + + // If this is a restored channel, then we want to avoid mutating the + // state at all, as it's impossible to do so in a protocol compliant + // manner. + if c.hasChanStatus(ChanStatusRestored) { + return ErrNoRestoredChannelMutation + } + + return c.Db.AdvanceCommitChainTail( + c, fwdPkg, updates, ourOutputIndex, theirOutputIndex, + ) +} + +// NextLocalHtlcIndex returns the next unallocated local htlc index. To ensure +// this always returns the next index that has been not been allocated, this +// will first try to examine any pending commitments, before falling back to the +// last locked-in remote commitment. +func (c *OpenChannel) NextLocalHtlcIndex() (uint64, error) { + // First, load the most recent commit diff that we initiated for the + // remote party. If no pending commit is found, this is not treated as + // a critical error, since we can always fall back. + pendingRemoteCommit, err := c.RemoteCommitChainTip() + if err != nil && err != ErrNoPendingCommit { + return 0, err + } + + // If a pending commit was found, its local htlc index will be at least + // as large as the one on our local commitment. + if pendingRemoteCommit != nil { + return pendingRemoteCommit.Commitment.LocalHtlcIndex, nil + } + + // Otherwise, fallback to using the local htlc index of their commitment. + return c.RemoteCommitment.LocalHtlcIndex, nil +} + +// 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 (c *OpenChannel) LoadFwdPkgs() ([]*FwdPkg, error) { + c.RLock() + defer c.RUnlock() + + return c.Db.LoadFwdPkgs(c) +} + +// 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 (c *OpenChannel) AckAddHtlcs(addRefs ...AddRef) error { + c.Lock() + defer c.Unlock() + + return c.Db.AckAddHtlcs(c, addRefs...) +} + +// 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 (c *OpenChannel) AckSettleFails(settleFailRefs ...SettleFailRef) error { + c.Lock() + defer c.Unlock() + + return c.Db.AckSettleFails(c, settleFailRefs...) +} + +// SetFwdFilter atomically sets the forwarding filter for the forwarding package +// identified by `height`. +func (c *OpenChannel) SetFwdFilter(height uint64, fwdFilter *PkgFilter) error { + c.Lock() + defer c.Unlock() + + return c.Db.SetFwdFilter(c, height, fwdFilter) +} + +// 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 (c *OpenChannel) RemoveFwdPkgs(heights ...uint64) error { + c.Lock() + defer c.Unlock() + + return c.Db.RemoveFwdPkgs(c, heights...) +} + +// 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 (c *OpenChannel) CommitmentHeight() (uint64, error) { + c.RLock() + defer c.RUnlock() + + return c.Db.CommitmentHeight(c) +} + +// 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 (c *OpenChannel) FindPreviousState( + updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { + + c.RLock() + defer c.RUnlock() + + return c.Db.FindPreviousState(c, updateNum) +} + +// CloseChannel closes a previously active Lightning channel. Closing a +// channel entails persisting a record of the close while either purging the +// nested per-channel state inline (synchronous backends like bbolt and etcd) +// or skipping the cascading delete on tombstone-enabled backends, where the +// outpoint-index flip to outpointClosed is the authoritative marker. The +// compact summary written to closedChannelBucket and the historical record +// under historicalChannelBucket are populated identically across both paths, +// so historical reads remain uniform regardless of backend. The optional set +// of channel statuses is OR'd into the chanStatus written to the historical +// bucket and is used to record close initiators. +func (c *OpenChannel) CloseChannel(summary *ChannelCloseSummary, + statuses ...ChannelStatus) error { + + c.Lock() + defer c.Unlock() + + return c.Db.CloseChannel(c, summary, statuses...) +} + +// Snapshot returns a read-only snapshot of the current channel state. This +// snapshot includes information concerning the current settled balance within +// the channel, metadata detailing total flows, and any outstanding HTLCs. +func (c *OpenChannel) Snapshot() *ChannelSnapshot { + c.RLock() + defer c.RUnlock() + + localCommit := c.LocalCommitment + snapshot := &ChannelSnapshot{ + RemoteIdentity: *c.IdentityPub, + ChannelPoint: c.FundingOutpoint, + Capacity: c.Capacity, + TotalMSatSent: c.TotalMSatSent, + TotalMSatReceived: c.TotalMSatReceived, + ChainHash: c.ChainHash, + ChannelCommitment: ChannelCommitment{ + LocalBalance: localCommit.LocalBalance, + RemoteBalance: localCommit.RemoteBalance, + CommitHeight: localCommit.CommitHeight, + CommitFee: localCommit.CommitFee, + }, + } + + localCommit.CustomBlob.WhenSome(func(blob tlv.Blob) { + blobCopy := make([]byte, len(blob)) + copy(blobCopy, blob) + + snapshot.ChannelCommitment.CustomBlob = fn.Some(blobCopy) + }) + + // Copy over the current set of HTLCs to ensure the caller can't mutate + // our internal state. + snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs)) + for i, h := range localCommit.Htlcs { + snapshot.Htlcs[i] = h.Copy() + } + + return snapshot +} + +// Copy returns a deep copy of the channel state. +func (c *OpenChannel) Copy() *OpenChannel { + c.RLock() + defer c.RUnlock() + + clone := &OpenChannel{ + ChanType: c.ChanType, + ChainHash: c.ChainHash, + FundingOutpoint: c.FundingOutpoint, + ShortChannelID: c.ShortChannelID, + IsPending: c.IsPending, + IsInitiator: c.IsInitiator, + chanStatus: c.chanStatus, + FundingBroadcastHeight: c.FundingBroadcastHeight, + ConfirmationHeight: c.ConfirmationHeight, + NumConfsRequired: c.NumConfsRequired, + ChannelFlags: c.ChannelFlags, + IdentityPub: c.IdentityPub, + Capacity: c.Capacity, + TotalMSatSent: c.TotalMSatSent, + TotalMSatReceived: c.TotalMSatReceived, + InitialLocalBalance: c.InitialLocalBalance, + InitialRemoteBalance: c.InitialRemoteBalance, + LocalChanCfg: c.LocalChanCfg, + RemoteChanCfg: c.RemoteChanCfg, + LocalCommitment: c.LocalCommitment.Copy(), + RemoteCommitment: c.RemoteCommitment.Copy(), + RemoteCurrentRevocation: c.RemoteCurrentRevocation, + RemoteNextRevocation: c.RemoteNextRevocation, + RevocationProducer: c.RevocationProducer, + RevocationStore: c.RevocationStore, + ThawHeight: c.ThawHeight, + LastWasRevoke: c.LastWasRevoke, + RevocationKeyLocator: c.RevocationKeyLocator, + confirmedScid: c.confirmedScid, + TapscriptRoot: c.TapscriptRoot, + } + + if c.FundingTxn != nil { + clone.FundingTxn = c.FundingTxn.Copy() + } + + if len(c.LocalShutdownScript) > 0 { + clone.LocalShutdownScript = make( + lnwire.DeliveryAddress, + len(c.LocalShutdownScript), + ) + copy(clone.LocalShutdownScript, c.LocalShutdownScript) + } + if len(c.RemoteShutdownScript) > 0 { + clone.RemoteShutdownScript = make( + lnwire.DeliveryAddress, + len(c.RemoteShutdownScript), + ) + copy(clone.RemoteShutdownScript, c.RemoteShutdownScript) + } + + if len(c.Memo) > 0 { + clone.Memo = make([]byte, len(c.Memo)) + copy(clone.Memo, c.Memo) + } + + c.CustomBlob.WhenSome(func(blob tlv.Blob) { + blobCopy := make([]byte, len(blob)) + copy(blobCopy, blob) + clone.CustomBlob = fn.Some(blobCopy) + }) + + return clone +} + +// 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 (c *OpenChannel) LatestCommitments() (*ChannelCommitment, *ChannelCommitment, error) { + return c.Db.LatestCommitments(c) +} + +// 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 (c *OpenChannel) RemoteRevocationStore() (shachain.Store, error) { + return c.Db.RemoteRevocationStore(c) +} + +// AbsoluteThawHeight determines a frozen channel's absolute thaw height. If the +// channel is not frozen, then 0 is returned. +func (c *OpenChannel) AbsoluteThawHeight() (uint32, error) { + // Only frozen channels have a thaw height. + if !c.ChanType.IsFrozen() && !c.ChanType.HasLeaseExpiration() { + return 0, nil + } + + // If the channel has the frozen bit set and it's thaw height is below + // the absolute threshold, then it's interpreted as a relative height to + // the chain's current height. + if c.ChanType.IsFrozen() && c.ThawHeight < AbsoluteThawHeightThreshold { + // We'll only known of the channel's short ID once it's + // confirmed. + if c.IsPending { + return 0, errors.New("cannot use relative thaw " + + "height for unconfirmed channel") + } + + // For non-zero-conf channels, this is the base height to use. + blockHeightBase := c.ShortChannelID.BlockHeight + + // If this is a zero-conf channel, the ShortChannelID will be + // an alias. + if c.IsZeroConf() { + if !c.ZeroConfConfirmed() { + return 0, errors.New("cannot use relative " + + "height for unconfirmed zero-conf " + + "channel") + } + + // Use the confirmed SCID's BlockHeight. + blockHeightBase = c.confirmedScid.BlockHeight + } + + return blockHeightBase + c.ThawHeight, nil + } + + return c.ThawHeight, nil +} + +// DeriveHeightHint derives the block height for the channel opening. +func (c *OpenChannel) DeriveHeightHint() uint32 { + // As a height hint, we'll try to use the opening height, but if the + // channel isn't yet open, then we'll use the height it was broadcast + // at. This may be an unconfirmed zero-conf channel. + heightHint := c.ShortChanID().BlockHeight + if heightHint == 0 { + heightHint = c.BroadcastHeight() + } + + // Since no zero-conf state is stored in a channel backup, the below + // logic will not be triggered for restored, zero-conf channels. Set + // the height hint for zero-conf channels. + if c.IsZeroConf() { + if c.ZeroConfConfirmed() { + // If the zero-conf channel is confirmed, we'll use the + // confirmed SCID's block height. + heightHint = c.ZeroConfRealScid().BlockHeight + } else { + // The zero-conf channel is unconfirmed. We'll need to + // use the FundingBroadcastHeight. + heightHint = c.BroadcastHeight() + } + } + + return heightHint +} From ae9cc13eb015ef157c35d2ff30806baa18d40003 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 12:26:59 -0300 Subject: [PATCH 32/33] chanstate: remove store generics Drop the temporary channel type parameter from the channel-state store interfaces now that OpenChannel lives in chanstate. The domain store facets now refer to *OpenChannel directly while retaining the same backend-independent shape. Update callers and compatibility aliases to use the concrete Store and ChannelShell types. --- channeldb/chanstate_assertions.go | 2 +- channeldb/db.go | 2 +- channelnotifier/channelnotifier.go | 4 +- chanrestore.go | 2 +- chanstate/interface.go | 134 +++++++++++++++-------------- chanstate/open_channel.go | 2 +- chanstate/open_channel_types.go | 4 +- contractcourt/breach_arbitrator.go | 3 +- funding/manager.go | 2 +- htlcswitch/link_test.go | 2 +- lnrpc/invoicesrpc/addinvoice.go | 2 +- lnrpc/invoicesrpc/config_active.go | 3 +- lnrpc/walletrpc/config_active.go | 3 +- peer/brontide.go | 2 +- server.go | 2 +- subrpcserver_config.go | 3 +- 16 files changed, 86 insertions(+), 86 deletions(-) diff --git a/channeldb/chanstate_assertions.go b/channeldb/chanstate_assertions.go index 453c02f04f..cce703b390 100644 --- a/channeldb/chanstate_assertions.go +++ b/channeldb/chanstate_assertions.go @@ -4,4 +4,4 @@ import "github.com/lightningnetwork/lnd/chanstate" // Compile-time assertion that ChannelStateDB satisfies the channel-state store // contract while the KV implementation still lives in channeldb. -var _ chanstate.Store[*OpenChannel] = (*ChannelStateDB)(nil) +var _ chanstate.Store = (*ChannelStateDB)(nil) diff --git a/channeldb/db.go b/channeldb/db.go index 4d563a9a70..7c67e23d19 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -1677,7 +1677,7 @@ func (c *ChannelStateDB) RepairLinkNodes(network wire.BitcoinNet) error { // ChannelShell is a shell of a channel that is meant to be used for channel // recovery purposes. -type ChannelShell = chanstate.ChannelShell[*OpenChannel] +type ChannelShell = chanstate.ChannelShell // RestoreChannelShells is a method that allows the caller to reconstruct the // state of an OpenChannel from the ChannelShell. We'll attempt to write the diff --git a/channelnotifier/channelnotifier.go b/channelnotifier/channelnotifier.go index 7648cc8c0a..06f3e67c0c 100644 --- a/channelnotifier/channelnotifier.go +++ b/channelnotifier/channelnotifier.go @@ -18,7 +18,7 @@ type ChannelNotifier struct { ntfnServer *subscribe.Server - chanDB chanstate.Store[*channeldb.OpenChannel] + chanDB chanstate.Store } // PendingOpenChannelEvent represents a new event where a new channel has @@ -98,7 +98,7 @@ type FundingTimeoutEvent struct { // New creates a new channel notifier. The ChannelNotifier gets channel // events from peers and from the chain arbitrator, and dispatches them to // its clients. -func New(chanDB chanstate.Store[*channeldb.OpenChannel]) *ChannelNotifier { +func New(chanDB chanstate.Store) *ChannelNotifier { return &ChannelNotifier{ ntfnServer: subscribe.NewServer(), chanDB: chanDB, diff --git a/chanrestore.go b/chanrestore.go index 129dbd34e1..407cdfbc7a 100644 --- a/chanrestore.go +++ b/chanrestore.go @@ -36,7 +36,7 @@ const ( // need the secret key chain in order obtain the prior shachain root so we can // verify the DLP protocol as initiated by the remote node. type chanDBRestorer struct { - db chanstate.OpenChannelStore[*channeldb.OpenChannel] + db chanstate.OpenChannelStore secretKeys keychain.SecretKeyRing diff --git a/chanstate/interface.go b/chanstate/interface.go index be073c87a1..e19f4de84e 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -21,38 +21,38 @@ import ( // concrete channeldb.ChannelStateDB type during the migration. Once the channel // state implementation moves into this package and the old concrete type is no // longer part of consumer-facing code, this name can be revisited. -type Store[Channel any] interface { +type Store interface { // OpenChannelStore owns open-channel records. - OpenChannelStore[Channel] + OpenChannelStore // HistoricalChannelStore owns the post-close historical channel view. - HistoricalChannelStore[Channel] + HistoricalChannelStore // OpenChannelLifecycleStore owns persisted lifecycle state for open // channel records. - OpenChannelLifecycleStore[Channel] + OpenChannelLifecycleStore // OpenChannelStatusStore owns persisted status flags for open channel // records. - OpenChannelStatusStore[Channel] + OpenChannelStatusStore // OpenChannelShutdownStore owns persisted shutdown state. - OpenChannelShutdownStore[Channel] + OpenChannelShutdownStore // OpenChannelCloseTxStore owns persisted closing transaction state. - OpenChannelCloseTxStore[Channel] + OpenChannelCloseTxStore // OpenChannelCommitmentStore owns persisted commitment state for open // channel records. - OpenChannelCommitmentStore[Channel] + OpenChannelCommitmentStore // OpenChannelFwdPkgStore owns forwarding packages tied to open // channel records. - OpenChannelFwdPkgStore[Channel] + OpenChannelFwdPkgStore // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. - ClosedChannelStore[Channel] + ClosedChannelStore // FinalHTLCStore owns final HTLC outcome data. FinalHTLCStore @@ -67,46 +67,46 @@ type Store[Channel any] interface { } // OpenChannelStore owns open-channel records. -type OpenChannelStore[Channel any] interface { +type OpenChannelStore interface { // 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. - FetchOpenChannels(nodeID *btcec.PublicKey) ([]Channel, error) + FetchOpenChannels(nodeID *btcec.PublicKey) ([]*OpenChannel, error) // FetchChannel attempts to locate a channel specified by the passed // channel point. If the channel cannot be found, then an error will // be returned. - FetchChannel(chanPoint wire.OutPoint) (Channel, error) + FetchChannel(chanPoint wire.OutPoint) (*OpenChannel, error) // FetchChannelByID attempts to locate a channel specified by the // passed channel ID. If the channel cannot be found, then an error // will be returned. - FetchChannelByID(id lnwire.ChannelID) (Channel, error) + FetchChannelByID(id lnwire.ChannelID) (*OpenChannel, error) // 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. - FetchAllChannels() ([]Channel, error) + FetchAllChannels() ([]*OpenChannel, error) // FetchAllOpenChannels will return all channels that have the // funding transaction confirmed, and is not waiting for a closing // transaction to be confirmed. - FetchAllOpenChannels() ([]Channel, error) + FetchAllOpenChannels() ([]*OpenChannel, error) // 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. - FetchPendingChannels() ([]Channel, error) + FetchPendingChannels() ([]*OpenChannel, error) // 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. - FetchWaitingCloseChannels() ([]Channel, error) + FetchWaitingCloseChannels() ([]*OpenChannel, error) // 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 @@ -120,193 +120,197 @@ type OpenChannelStore[Channel any] interface { // finally create an edge within the graph for the channel as well. // This method is idempotent, so repeated calls with the same set of // channel shells won't modify the database after the initial call. - RestoreChannelShells(channelShells ...*ChannelShell[Channel]) error + RestoreChannelShells(channelShells ...*ChannelShell) error } // HistoricalChannelStore owns the post-close historical channel view. -type HistoricalChannelStore[Channel any] interface { +type HistoricalChannelStore interface { // FetchHistoricalChannel fetches open channel data from the // historical channel bucket. - FetchHistoricalChannel(outPoint *wire.OutPoint) (Channel, error) + FetchHistoricalChannel(outPoint *wire.OutPoint) (*OpenChannel, error) } // OpenChannelLifecycleStore owns persisted lifecycle state for open channel // records. -type OpenChannelLifecycleStore[Channel any] interface { +type OpenChannelLifecycleStore interface { // SyncPendingChannel writes a pending channel to the store and records // the funding broadcast height. - SyncPendingChannel(channel Channel, addr net.Addr, + SyncPendingChannel(channel *OpenChannel, addr net.Addr, pendingHeight uint32) error // RefreshChannel updates the in-memory channel state using the latest // state observed on disk. - RefreshChannel(channel Channel) error + RefreshChannel(channel *OpenChannel) error // MarkChannelConfirmationHeight updates the channel's confirmation // height once the channel opening transaction receives one // confirmation. - MarkChannelConfirmationHeight(channel Channel, height uint32) error + MarkChannelConfirmationHeight(channel *OpenChannel, height uint32) error // MarkChannelCloseConfirmationHeight updates the channel's close // confirmation height when the closing transaction is first detected // in a block. - MarkChannelCloseConfirmationHeight(channel Channel, + MarkChannelCloseConfirmationHeight(channel *OpenChannel, height fn.Option[uint32]) error // MarkChannelOpen marks a channel as fully open given a locator that // uniquely describes its location within the chain. - MarkChannelOpen(channel Channel, openLoc lnwire.ShortChannelID) error + MarkChannelOpen(channel *OpenChannel, + openLoc lnwire.ShortChannelID) error // MarkChannelRealScid marks the zero-conf channel's confirmed // ShortChannelID. - MarkChannelRealScid(channel Channel, + MarkChannelRealScid(channel *OpenChannel, realScid lnwire.ShortChannelID) error // MarkChannelScidAliasNegotiated marks that the scid-alias feature // bit was negotiated during the lifetime of the channel. - MarkChannelScidAliasNegotiated(channel Channel) error + MarkChannelScidAliasNegotiated(channel *OpenChannel) error } // OpenChannelStatusStore owns persisted status flags for open channel records. -type OpenChannelStatusStore[Channel any] interface { +type OpenChannelStatusStore interface { // ApplyChannelStatus adds the target status to the channel's // persisted status bit field. - ApplyChannelStatus(channel Channel, status ChannelStatus) error + ApplyChannelStatus(channel *OpenChannel, status ChannelStatus) error // ClearChannelStatus clears the target status from the channel's // persisted status bit field. - ClearChannelStatus(channel Channel, status ChannelStatus) error + ClearChannelStatus(channel *OpenChannel, status ChannelStatus) error // MarkChannelDataLoss marks the channel as local-data-loss and stores // the commit point needed if the remote force closes. - MarkChannelDataLoss(channel Channel, + MarkChannelDataLoss(channel *OpenChannel, commitPoint *btcec.PublicKey) error // FetchChannelDataLossCommitPoint retrieves the commit point stored // when the channel was marked as local-data-loss. - FetchChannelDataLossCommitPoint(channel Channel) ( + FetchChannelDataLossCommitPoint(channel *OpenChannel) ( *btcec.PublicKey, error) // MarkChannelBorked marks the channel as irreconcilable. - MarkChannelBorked(channel Channel) error + MarkChannelBorked(channel *OpenChannel) error } // OpenChannelShutdownStore owns persisted shutdown state. -type OpenChannelShutdownStore[Channel any] interface { +type OpenChannelShutdownStore interface { // StoreChannelShutdownInfo persists the ShutdownInfo for the target // channel. - StoreChannelShutdownInfo(channel Channel, info *ShutdownInfo) error + StoreChannelShutdownInfo(channel *OpenChannel, info *ShutdownInfo) error // FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the // target channel. - FetchChannelShutdownInfo(channel Channel) (fn.Option[ShutdownInfo], - error) + FetchChannelShutdownInfo(channel *OpenChannel) ( + fn.Option[ShutdownInfo], error) } // OpenChannelCloseTxStore owns persisted closing transaction state. -type OpenChannelCloseTxStore[Channel any] interface { +type OpenChannelCloseTxStore interface { // MarkChannelCommitmentBroadcasted marks the channel as having a // commitment transaction broadcast. - MarkChannelCommitmentBroadcasted(channel Channel, closeTx *wire.MsgTx, - closer lntypes.ChannelParty) error + MarkChannelCommitmentBroadcasted(channel *OpenChannel, + closeTx *wire.MsgTx, closer lntypes.ChannelParty) error // MarkChannelCoopBroadcasted marks the channel as having a // cooperative close transaction broadcast. - MarkChannelCoopBroadcasted(channel Channel, closeTx *wire.MsgTx, + MarkChannelCoopBroadcasted(channel *OpenChannel, closeTx *wire.MsgTx, closer lntypes.ChannelParty) error // FetchChannelBroadcastedCommitment fetches the stored unilateral // closing transaction. - FetchChannelBroadcastedCommitment(channel Channel) (*wire.MsgTx, + FetchChannelBroadcastedCommitment(channel *OpenChannel) (*wire.MsgTx, error) // FetchChannelBroadcastedCooperative fetches the stored cooperative // closing transaction. - FetchChannelBroadcastedCooperative(channel Channel) (*wire.MsgTx, + FetchChannelBroadcastedCooperative(channel *OpenChannel) (*wire.MsgTx, error) } // OpenChannelCommitmentStore owns persisted commitment state for open channel // records. -type OpenChannelCommitmentStore[Channel any] interface { +type OpenChannelCommitmentStore interface { // UpdateChannelCommitment updates the local commitment state. It // locks in pending local updates received from the remote party and // persists remote log updates that have been acked, but not signed // for yet. The returned map contains all HTLC resolutions locked into // this commitment, keyed by HTLC index. - UpdateChannelCommitment(channel Channel, + UpdateChannelCommitment(channel *OpenChannel, newCommitment *ChannelCommitment, unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) // AppendRemoteCommitChain appends a new CommitDiff to the remote // party's commitment chain. This is used after preparing a new remote // commitment state, before transmitting it to the remote party. - AppendRemoteCommitChain(channel Channel, diff *CommitDiff) error + AppendRemoteCommitChain(channel *OpenChannel, diff *CommitDiff) error // RemoteCommitChainTip returns the "tip" of the current remote // commitment chain. - RemoteCommitChainTip(channel Channel) (*CommitDiff, error) + RemoteCommitChainTip(channel *OpenChannel) (*CommitDiff, error) // UnsignedAckedUpdates retrieves the persisted unsigned acked remote // log updates that still need to be signed for. - UnsignedAckedUpdates(channel Channel) ([]LogUpdate, error) + UnsignedAckedUpdates(channel *OpenChannel) ([]LogUpdate, error) // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local // log updates that the remote still needs to sign for. - RemoteUnsignedLocalUpdates(channel Channel) ([]LogUpdate, error) + RemoteUnsignedLocalUpdates(channel *OpenChannel) ([]LogUpdate, error) // InsertNextRevocation inserts the next commitment point into the // persisted channel state. - InsertNextRevocation(channel Channel, revKey *btcec.PublicKey) error + InsertNextRevocation(channel *OpenChannel, + revKey *btcec.PublicKey) error // AdvanceCommitChainTail records the new state transition within the // revocation log and promotes the pending remote commitment to the // current remote commitment. - AdvanceCommitChainTail(channel Channel, fwdPkg *FwdPkg, + AdvanceCommitChainTail(channel *OpenChannel, fwdPkg *FwdPkg, updates []LogUpdate, ourOutputIndex, theirOutputIndex uint32) error // CommitmentHeight returns the current persisted commitment height. - CommitmentHeight(channel Channel) (uint64, error) + CommitmentHeight(channel *OpenChannel) (uint64, error) // LatestCommitments returns the two latest commitments for both the // local and remote party. - LatestCommitments(channel Channel) (*ChannelCommitment, + LatestCommitments(channel *OpenChannel) (*ChannelCommitment, *ChannelCommitment, error) // RemoteRevocationStore returns the most up to date commitment version // of the revocation storage tree for the remote party. - RemoteRevocationStore(channel Channel) (shachain.Store, error) + RemoteRevocationStore(channel *OpenChannel) (shachain.Store, error) // FindPreviousState scans through the append-only log in an attempt to // recover the previous channel state indicated by the update number. - FindPreviousState(channel Channel, updateNum uint64) ( + FindPreviousState(channel *OpenChannel, updateNum uint64) ( *RevocationLog, *ChannelCommitment, error) } // OpenChannelFwdPkgStore owns forwarding packages tied to open channel records. -type OpenChannelFwdPkgStore[Channel any] interface { +type OpenChannelFwdPkgStore interface { // LoadFwdPkgs loads forwarding packages that have not been processed. - LoadFwdPkgs(channel Channel) ([]*FwdPkg, error) + LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, error) // AckAddHtlcs marks add HTLCs in forwarding packages as resolved. - AckAddHtlcs(channel Channel, addRefs ...AddRef) error + AckAddHtlcs(channel *OpenChannel, addRefs ...AddRef) error // AckSettleFails marks settles or fails as delivered to the incoming // link. - AckSettleFails(channel Channel, settleFailRefs ...SettleFailRef) error + AckSettleFails(channel *OpenChannel, + settleFailRefs ...SettleFailRef) error // SetFwdFilter writes the forwarding filter for the forwarding package // identified by height. - SetFwdFilter(channel Channel, height uint64, fwdFilter *PkgFilter) error + SetFwdFilter(channel *OpenChannel, height uint64, + fwdFilter *PkgFilter) error // RemoveFwdPkgs removes forwarding packages by remote commitment // height. - RemoveFwdPkgs(channel Channel, heights ...uint64) error + RemoveFwdPkgs(channel *OpenChannel, heights ...uint64) error } // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. -type ClosedChannelStore[Channel any] interface { +type ClosedChannelStore interface { // 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 @@ -340,7 +344,7 @@ type ClosedChannelStore[Channel any] interface { // FetchClosedChannel and FetchClosedChannelForID. Any ChannelStatus // values are merged into the archived summary. Returns // ErrChannelCloseSummaryNil if summary is nil. - CloseChannel(channel Channel, summary *ChannelCloseSummary, + CloseChannel(channel *OpenChannel, summary *ChannelCloseSummary, statuses ...ChannelStatus) error // AbandonChannel attempts to remove the target channel from the open diff --git a/chanstate/open_channel.go b/chanstate/open_channel.go index aadf70d0b5..4bf293c513 100644 --- a/chanstate/open_channel.go +++ b/chanstate/open_channel.go @@ -216,7 +216,7 @@ type OpenChannel struct { // channels through the channeldb compatibility alias. The store // interface keeps receiver methods backend independent while the KV // implementation remains in channeldb. - Db Store[*OpenChannel] + Db Store // TODO(roasbeef): just need to store local and remote HTLC's? diff --git a/chanstate/open_channel_types.go b/chanstate/open_channel_types.go index 0a3e279b29..90ab06bc62 100644 --- a/chanstate/open_channel_types.go +++ b/chanstate/open_channel_types.go @@ -5,12 +5,12 @@ import "net" // ChannelShell is a shell of a channel that is meant to be used for channel // recovery purposes. It contains a minimal OpenChannel instance along with // addresses for that target node. -type ChannelShell[Channel any] struct { +type ChannelShell struct { // NodeAddrs the set of addresses that this node has known to be // reachable at in the past. NodeAddrs []net.Addr // Chan is a shell of an OpenChannel, it contains only the items // required to restore the channel on disk. - Chan Channel + Chan *OpenChannel } diff --git a/contractcourt/breach_arbitrator.go b/contractcourt/breach_arbitrator.go index 9d00540a5c..2c12f25598 100644 --- a/contractcourt/breach_arbitrator.go +++ b/contractcourt/breach_arbitrator.go @@ -14,7 +14,6 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" @@ -143,7 +142,7 @@ type BreachConfig struct { // DB provides access to the user's closed channels, allowing the breach // arbiter to determine how it should respond to channel closure. - DB chanstate.ClosedChannelStore[*channeldb.OpenChannel] + DB chanstate.ClosedChannelStore // Estimator is used by the breach arbiter to determine an appropriate // fee level when generating, signing, and broadcasting sweep diff --git a/funding/manager.go b/funding/manager.go index d1b319c5d4..2dcd4f1b73 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -387,7 +387,7 @@ type Config struct { // ChannelDB is the database that keeps track of channel state used by // the funding flow. - ChannelDB chanstate.Store[*channeldb.OpenChannel] + ChannelDB chanstate.Store // SignMessage signs an arbitrary message with a given public key. The // actual digest signed is the double sha-256 of the message. In the diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index e54f620b64..ccc8f61d12 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -5770,7 +5770,7 @@ func TestChannelLinkCleanupSpuriousResponses(t *testing.T) { } type mockFailLoadFwdPkgStore struct { - cstate.Store[*channeldb.OpenChannel] + cstate.Store } func (m *mockFailLoadFwdPkgStore) LoadFwdPkgs( diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index 54735e8680..b4d39a99c5 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -72,7 +72,7 @@ type AddInvoiceConfig struct { DefaultCLTVExpiry uint32 // ChanDB is used to access open channel state. - ChanDB chanstate.OpenChannelStore[*channeldb.OpenChannel] + ChanDB chanstate.OpenChannelStore // Graph gives the invoice server access to various graph related // queries. diff --git a/lnrpc/invoicesrpc/config_active.go b/lnrpc/invoicesrpc/config_active.go index bb20d173a9..233aa59275 100644 --- a/lnrpc/invoicesrpc/config_active.go +++ b/lnrpc/invoicesrpc/config_active.go @@ -5,7 +5,6 @@ package invoicesrpc import ( "github.com/btcsuite/btcd/chaincfg" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnwire" @@ -58,7 +57,7 @@ type Config struct { // ChanStateDB is a possibly replicated db instance which contains open // channel state. - ChanStateDB chanstate.OpenChannelStore[*channeldb.OpenChannel] + ChanStateDB chanstate.OpenChannelStore // GenInvoiceFeatures returns a feature containing feature bits that // should be advertised on freshly generated invoices. diff --git a/lnrpc/walletrpc/config_active.go b/lnrpc/walletrpc/config_active.go index 97bbb6411c..e0c9c684a4 100644 --- a/lnrpc/walletrpc/config_active.go +++ b/lnrpc/walletrpc/config_active.go @@ -6,7 +6,6 @@ package walletrpc import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcwallet/wallet" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -80,5 +79,5 @@ type Config struct { CoinSelectionStrategy wallet.CoinSelectionStrategy // ChanStateDB is the reference to the open channel store. - ChanStateDB chanstate.OpenChannelStore[*channeldb.OpenChannel] + ChanStateDB chanstate.OpenChannelStore } diff --git a/peer/brontide.go b/peer/brontide.go index 6f95032932..f7a01cd11f 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -261,7 +261,7 @@ type Config struct { InterceptSwitch *htlcswitch.InterceptableSwitch // ChannelDB is used to fetch channel state needed by the peer. - ChannelDB chanstate.Store[*channeldb.OpenChannel] + ChannelDB chanstate.Store // ChannelGraph is a pointer to the channel graph which is used to // query information about the set of known active channels. diff --git a/server.go b/server.go index b49600ff8a..45992c464c 100644 --- a/server.go +++ b/server.go @@ -326,7 +326,7 @@ type server struct { graphDB *graphdb.ChannelGraph v1Graph *graphdb.VersionedGraph - chanStateDB chanstate.Store[*channeldb.OpenChannel] + chanStateDB chanstate.Store linkNodeDB *channeldb.LinkNodeDB addrSource channeldb.AddrSource diff --git a/subrpcserver_config.go b/subrpcserver_config.go index efb71f7180..856553c38f 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -11,7 +11,6 @@ import ( "github.com/lightningnetwork/lnd/aliasmgr" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/chainreg" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/fn/v2" graphdb "github.com/lightningnetwork/lnd/graph/db" @@ -116,7 +115,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, routerBackend *routerrpc.RouterBackend, nodeSigner *netann.NodeSigner, graphDB *graphdb.ChannelGraph, - chanStateDB chanstate.Store[*channeldb.OpenChannel], + chanStateDB chanstate.Store, sweeper *sweep.UtxoSweeper, tower *watchtower.Standalone, towerClientMgr *wtclient.Manager, From dc0cf06a4e9c9b9c719f127a38412cbcf2483dff Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 15 May 2026 12:53:19 -0300 Subject: [PATCH 33/33] chanstate: fix htlc copy Copy all HTLC fields when cloning channel commitment state. The old copy method only copied a subset of scalar fields and copied into nil slices for Signature and ExtraData. Allocate those slices and deep-copy custom record values so snapshots and channel copies retain complete HTLC metadata. --- chanstate/commitment.go | 25 +++++++++++--- chanstate/commitment_test.go | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 chanstate/commitment_test.go diff --git a/chanstate/commitment.go b/chanstate/commitment.go index c653b3cd0d..0cb49e0231 100644 --- a/chanstate/commitment.go +++ b/chanstate/commitment.go @@ -207,12 +207,29 @@ func (h *HTLC) Copy() HTLC { Amt: h.Amt, RefundTimeout: h.RefundTimeout, OutputIndex: h.OutputIndex, + RHash: h.RHash, + OnionBlob: h.OnionBlob, + HtlcIndex: h.HtlcIndex, + LogIndex: h.LogIndex, + } + if len(h.Signature) > 0 { + clone.Signature = make([]byte, len(h.Signature)) + copy(clone.Signature, h.Signature) + } + if len(h.ExtraData) > 0 { + clone.ExtraData = make(lnwire.ExtraOpaqueData, len(h.ExtraData)) + copy(clone.ExtraData, h.ExtraData) } - copy(clone.Signature, h.Signature) - copy(clone.RHash[:], h.RHash[:]) - copy(clone.ExtraData, h.ExtraData) clone.BlindingPoint = h.BlindingPoint - clone.CustomRecords = h.CustomRecords.Copy() + if h.CustomRecords != nil { + clone.CustomRecords = make( + lnwire.CustomRecords, len(h.CustomRecords), + ) + for k, v := range h.CustomRecords { + clone.CustomRecords[k] = make([]byte, len(v)) + copy(clone.CustomRecords[k], v) + } + } return clone } diff --git a/chanstate/commitment_test.go b/chanstate/commitment_test.go new file mode 100644 index 0000000000..ab1794d771 --- /dev/null +++ b/chanstate/commitment_test.go @@ -0,0 +1,66 @@ +package chanstate + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/require" +) + +func TestHTLCCopy(t *testing.T) { + t.Parallel() + + _, blindingPoint := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{1}, 32)) + + var rHash [32]byte + copy(rHash[:], bytes.Repeat([]byte{2}, len(rHash))) + + var onionBlob [lnwire.OnionPacketSize]byte + copy(onionBlob[:], bytes.Repeat([]byte{3}, len(onionBlob))) + + htlc := HTLC{ + Signature: []byte{4, 5, 6}, + RHash: rHash, + Amt: 1000, + RefundTimeout: 144, + OutputIndex: 3, + Incoming: true, + OnionBlob: onionBlob, + HtlcIndex: 42, + LogIndex: 43, + ExtraData: lnwire.ExtraOpaqueData{7, 8, 9}, + BlindingPoint: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType]( + blindingPoint, + ), + ), + CustomRecords: lnwire.CustomRecords{ + lnwire.MinCustomRecordsTlvType: []byte{10, 11, 12}, + }, + } + + clone := htlc.Copy() + require.Equal(t, htlc, clone) + + clone.Signature[0] = 0 + require.Equal(t, byte(4), htlc.Signature[0]) + + clone.ExtraData[0] = 0 + require.Equal(t, byte(7), htlc.ExtraData[0]) + + clone.CustomRecords[lnwire.MinCustomRecordsTlvType] = []byte{0} + require.Equal( + t, []byte{10, 11, 12}, + htlc.CustomRecords[lnwire.MinCustomRecordsTlvType], + ) + + clone = htlc.Copy() + clone.CustomRecords[lnwire.MinCustomRecordsTlvType][0] = 0 + require.Equal( + t, []byte{10, 11, 12}, + htlc.CustomRecords[lnwire.MinCustomRecordsTlvType], + ) +}