diff --git a/channeldb/channel.go b/channeldb/channel.go index 127e0ac9c4d..038cf5cafaf 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2,28 +2,20 @@ package channeldb import ( "bytes" - "crypto/hmac" - "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "net" - "strconv" - "strings" - "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" "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" - "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" @@ -33,16 +25,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. @@ -177,50 +171,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 ( @@ -229,6 +222,27 @@ const ( indexStatusType tlv.Type = 0 ) +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 + + // 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 + + // 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 // 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 @@ -400,261 +414,78 @@ 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 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 -} +// 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. 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 -} +// 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 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 -} +// 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. @@ -703,97 +534,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) { @@ -805,137 +554,43 @@ 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 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 @@ -950,421 +605,70 @@ 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 - - // 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. - // - // 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] - - // TODO(roasbeef): eww - Db *ChannelStateDB - - // 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 -} - -// 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.putChanStatus(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.clearChanStatus(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 -} - -// 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 -} - -// 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.SetConfirmedScidForStore(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.ConfirmedScidForStore(), ), 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), ) @@ -1373,15 +677,13 @@ func (c *OpenChannel) extractTlvData() 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() - - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { +// 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 @@ -1389,30 +691,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 @@ -1553,9 +852,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 { @@ -1645,181 +944,145 @@ func (c *OpenChannel) fullSync(tx kvdb.RwTx) 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() +// MarkChannelConfirmationHeight updates the channel's confirmation height once +// the channel opening transaction receives one confirmation. +func (c *ChannelStateDB) MarkChannelConfirmationHeight(channel *OpenChannel, + height uint32) 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.ConfirmationHeight = height + diskChannel.ConfirmationHeight = height - return putOpenChannel(chanBucket, channel) - }, func() {}); 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]()) + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) } -// 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 { +// 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 { - c.Lock() - defer c.Unlock() - - 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.CloseConfirmationHeight = height - - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } - - c.CloseConfirmationHeight = height + diskChannel.CloseConfirmationHeight = height - return nil + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) } -// 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 + diskChannel.IsPending = false + diskChannel.ShortChannelID = openLoc - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } - - 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 -// should only be done if IsZeroConf returns true. -func (c *OpenChannel) MarkRealScid(realScid lnwire.ShortChannelID) error { - c.Lock() - defer c.Unlock() +// MarkChannelRealScid marks the zero-conf channel's confirmed ShortChannelID. +func (c *ChannelStateDB) MarkChannelRealScid(channel *OpenChannel, + realScid 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.confirmedScid = realScid - - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } - - c.confirmedScid = realScid + diskChannel.SetConfirmedScidForStore(realScid) - return nil + return putOpenChannel(chanBucket, diskChannel) + }, func() {}) } -// MarkScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in-memory and -// in the database. -func (c *OpenChannel) MarkScidAliasNegotiated() error { - c.Lock() - defer c.Unlock() +// MarkChannelScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in the +// database. +func (c *ChannelStateDB) MarkChannelScidAliasNegotiated( + channel *OpenChannel) 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.ChanType |= ScidAliasFeatureBit - return putOpenChannel(chanBucket, channel) - }, func() {}); err != nil { - return err - } - - c.ChanType |= ScidAliasFeatureBit + diskChannel.ChanType |= ScidAliasFeatureBit - return nil + return putOpenChannel(chanBucket, diskChannel) + }, 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() +// 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 { @@ -1830,17 +1093,20 @@ 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) { +// 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: @@ -1870,242 +1136,35 @@ func (c *OpenChannel) DataLossCommitPoint() (*btcec.PublicKey, error) { 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.putChanStatus(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 +// MarkChannelBorked marks the channel as irreconcilable. +func (c *ChannelStateDB) MarkChannelBorked(channel *OpenChannel) error { + return c.ApplyChannelStatus(channel, ChanStatusBorked) } 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) + // DeriveMusig2Shachain derives a shachain producer for the taproot + // channel from normal shachain revocation root. + DeriveMusig2Shachain = cstate.DeriveMusig2Shachain - return musig2.GenNonces(pubKeyOpt, shaChainRand) -} - -// 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() + // NewMusigVerificationNonce generates the local or verification nonce + // for another musig2 session. + NewMusigVerificationNonce = cstate.NewMusigVerificationNonce +) - return c.storeShutdownInfo(info) -} +// StoreChannelShutdownInfo persists the ShutdownInfo for the target channel. +func (c *ChannelStateDB) StoreChannelShutdownInfo(channel *OpenChannel, + info *ShutdownInfo) error { -// storeShutdownInfo serialises the ShutdownInfo and persists it under the -// 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 } - 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 @@ -2115,17 +1174,16 @@ func (c *OpenChannel) storeShutdownInfo(info *ShutdownInfo) error { }, 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() +// 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: @@ -2156,60 +1214,54 @@ func (c *OpenChannel) ShutdownInfo() (fn.Option[ShutdownInfo], error) { 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.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, +// 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, ) } -// 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 { +// 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. @@ -2234,29 +1286,36 @@ func (c *OpenChannel) markBroadcasted(status ChannelStatus, key []byte, status |= ChanStatusRemoteCloseInitiator } - return c.putChanStatus(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) +// 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) +// FetchChannelBroadcastedCooperative fetches the stored cooperative closing +// transaction. +func (c *ChannelStateDB) FetchChannelBroadcastedCooperative( + channel *OpenChannel) (*wire.MsgTx, error) { + + return c.getClosingTx(channel, coopCloseTxKey) } -// 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) { +// 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: @@ -2282,30 +1341,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 { - if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) 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.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.ChannelStatusForStore() | status + diskChannel.SetChannelStatusForStore(status) - if err := putOpenChannel(chanBucket, channel); err != nil { + if err := putOpenChannel(chanBucket, diskChannel); err != nil { return err } @@ -2326,36 +1396,43 @@ func (c *OpenChannel) putChanStatus(status ChannelStatus, } // Update the in-memory representation to keep it in sync with the DB. - c.chanStatus = status + channel.SetChannelStatusForStore(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.ChannelStatusForStore() & ^status + diskChannel.SetChannelStatusForStore(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.SetChannelStatusForStore(status) return nil } @@ -2437,38 +1514,28 @@ func fetchOpenChannel(chanBucket kvdb.RBucket, err) } - channel.Packager = NewChannelPackager(channel.ShortChannelID) - 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() +// 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 { - c.FundingBroadcastHeight = pendingHeight + channel.FundingBroadcastHeight = pendingHeight - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return syncNewChannel(tx, c, []net.Addr{addr}) + 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 := c.fullSync(tx); err != nil { + if err := fullSyncOpenChannel(tx, c); err != nil { return err } @@ -2488,8 +1555,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? @@ -2497,36 +1564,17 @@ func syncNewChannel(tx kvdb.RwTx, c *OpenChannel, addrs []net.Addr) error { 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, +// UpdateChannelCommitment updates the local commitment state. +func (c *ChannelStateDB) UpdateChannelCommitment(channel *OpenChannel, + 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 - } - 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 @@ -2534,7 +1582,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 := isChannelBorked(channel, chanBucket) if err != nil { return err } @@ -2542,7 +1590,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) } @@ -2597,9 +1645,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 @@ -2648,8 +1696,6 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, return nil, err } - c.LocalCommitment = *newCommitment - return finalHtlcs, nil } @@ -2699,131 +1745,7 @@ 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 -} - -// 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 @@ -2831,7 +1753,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]) { @@ -2847,12 +1769,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 } @@ -2904,7 +1826,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 } @@ -2996,7 +1918,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 } } @@ -3004,37 +1926,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. -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) @@ -3050,61 +1941,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)) @@ -3181,7 +2017,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) } @@ -3255,33 +2091,26 @@ 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 } -// 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() +func newChannelPackager(channel *OpenChannel) *ChannelPackager { + return NewChannelPackager(channel.ShortChannelID) +} - // 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 - } +// 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.Db.backend, func(tx kvdb.RwTx) 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 @@ -3289,7 +2118,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 := isChannelBorked(channel, chanBucket) if err != nil { return err } @@ -3302,7 +2131,9 @@ 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...) + packager := newChannelPackager(channel) + + err = packager.AckAddHtlcs(tx, diff.AddAcks...) if err != nil { return err } @@ -3312,7 +2143,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 = packager.AckSettleFails( + tx, diff.SettleFailAcks..., + ) if err != nil { return err } @@ -3340,16 +2173,15 @@ func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error { } // 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) { +// 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: @@ -3384,11 +2216,14 @@ 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) { +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: @@ -3418,11 +2253,14 @@ 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) { +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: @@ -3451,28 +2289,23 @@ func (c *OpenChannel) RemoteUnsignedLocalUpdates() ([]LogUpdate, error) { 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() +// InsertNextRevocation inserts the next commitment point into the persisted +// channel state. +func (c *ChannelStateDB) InsertNextRevocation(channel *OpenChannel, + revKey *btcec.PublicKey) error { - c.RemoteNextRevocation = revKey + channel.RemoteNextRevocation = revKey - 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 } - return putChanRevocationState(chanBucket, c) + return putChanRevocationState(chanBucket, channel) }, func() {}) if err != nil { return err @@ -3481,33 +2314,19 @@ func (c *OpenChannel) InsertNextRevocation(revKey *btcec.PublicKey) error { 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 - } +// 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 @@ -3515,7 +2334,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 := isChannelBorked(channel, chanBucket) if err != nil { return err } @@ -3526,7 +2345,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 } @@ -3565,8 +2384,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 @@ -3575,7 +2394,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 := newChannelPackager(channel).AddFwdPkg(tx, fwdPkg); err != nil { return err } @@ -3647,21 +2466,13 @@ 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 } // 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. @@ -3682,40 +2493,16 @@ 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() +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 = newChannelPackager(channel).LoadFwdPkgs(tx) return err }, func() { fwdPkgs = nil @@ -3729,12 +2516,11 @@ func (c *OpenChannel) LoadFwdPkgs() ([]*FwdPkg, error) { // 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() +func (c *ChannelStateDB) AckAddHtlcs(channel *OpenChannel, + addRefs ...AddRef) error { - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return c.Packager.AckAddHtlcs(tx, addRefs...) + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + return newChannelPackager(channel).AckAddHtlcs(tx, addRefs...) }, func() {}) } @@ -3742,23 +2528,25 @@ func (c *OpenChannel) AckAddHtlcs(addRefs ...AddRef) error { // 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() +func (c *ChannelStateDB) AckSettleFails(channel *OpenChannel, + settleFailRefs ...SettleFailRef) error { - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return c.Packager.AckSettleFails(tx, settleFailRefs...) + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + return newChannelPackager(channel).AckSettleFails( + tx, settleFailRefs..., + ) }, func() {}) } // SetFwdFilter atomically sets the forwarding filter for the forwarding package // identified by `height`. -func (c *OpenChannel) SetFwdFilter(height uint64, fwdFilter *PkgFilter) error { - c.Lock() - defer c.Unlock() +func (c *ChannelStateDB) SetFwdFilter(channel *OpenChannel, height uint64, + fwdFilter *PkgFilter) error { - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - return c.Packager.SetFwdFilter(tx, height, fwdFilter) + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + return newChannelPackager(channel).SetFwdFilter( + tx, height, fwdFilter, + ) }, func() {}) } @@ -3767,13 +2555,14 @@ func (c *OpenChannel) SetFwdFilter(height uint64, fwdFilter *PkgFilter) error { // 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() +func (c *ChannelStateDB) RemoveFwdPkgs(channel *OpenChannel, + heights ...uint64) error { + + return kvdb.Update(c.backend, func(tx kvdb.RwTx) error { + packager := newChannelPackager(channel) - return kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { for _, height := range heights { - err := c.Packager.RemovePkg(tx, height) + err := packager.RemovePkg(tx, height) if err != nil { return err } @@ -3784,26 +2573,22 @@ func (c *OpenChannel) RemoveFwdPkgs(heights ...uint64) error { } // 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() +// 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 @@ -3835,16 +2620,16 @@ func (c *OpenChannel) revocationLogTailCommitHeight() (uint64, error) { // 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() +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 @@ -3872,18 +2657,16 @@ func (c *OpenChannel) CommitmentHeight() (uint64, error) { // 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( +func (c *ChannelStateDB) FindPreviousState(channel *OpenChannel, updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { - c.RLock() - defer c.RUnlock() - 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 @@ -3909,141 +2692,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 -} - -// 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...) -} +// was closed. +type ChannelCloseSummary = cstate.ChannelCloseSummary // CloseChannel closes the supplied channel via the strategy selected at DB // construction. On synchronous backends the channel's nested state — the @@ -4164,7 +2845,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 { @@ -4197,7 +2880,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 } @@ -4264,264 +2947,57 @@ 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 -} - -// 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, - Packager: c.Packager, - 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 -} +// ChannelSnapshot is a frozen snapshot of the current channel state. +type ChannelSnapshot = cstate.ChannelSnapshot // 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) { - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { +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 // 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) { - err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { +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 -} - -// 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 + return channel.RevocationStore, nil } func putChannelCloseSummary(tx kvdb.RwTx, chanID []byte, @@ -4700,7 +3176,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 { @@ -4708,7 +3184,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, @@ -4731,7 +3207,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) } @@ -4821,7 +3297,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) } @@ -4832,7 +3308,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 } @@ -4887,16 +3363,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. @@ -4935,9 +3413,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) - - channel.Packager = NewChannelPackager(channel.ShortChannelID) + amendOpenChannelTlvData(channel, auxData) // Finally, read the optional shutdown scripts. if err := getOptionalUpfrontShutdownScript( @@ -5001,7 +3477,7 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, "chan aux data: %w", err) } - chanCommit.amendTlvData(auxData) + amendCommitTlvData(&chanCommit, auxData) return chanCommit, nil } @@ -5011,7 +3487,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 } @@ -5160,40 +3636,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/channeldb/channel_test.go b/channeldb/channel_test.go index 47504067780..6e1750b3e28 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), @@ -879,7 +878,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 +921,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") @@ -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 { @@ -1559,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()) } } }) @@ -1642,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/chanstate_assertions.go b/channeldb/chanstate_assertions.go new file mode 100644 index 00000000000..cce703b3906 --- /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 = (*ChannelStateDB)(nil) diff --git a/channeldb/close_channel_test.go b/channeldb/close_channel_test.go index 3a940b78dec..43466a44b1f 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/channeldb/db.go b/channeldb/db.go index 3f0036b112c..7c67e23d19c 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 // RestoreChannelShells is a method that allows the caller to reconstruct the // state of an OpenChannel from the ChannelShell. We'll attempt to write the @@ -1705,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 @@ -1713,7 +1704,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/channeldb/db_test.go b/channeldb/db_test.go index 277820b10cd..c973eade727 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/channeldb/forwarding_package.go b/channeldb/forwarding_package.go index c393a53b37f..31ec1cc748e 100644 --- a/channeldb/forwarding_package.go +++ b/channeldb/forwarding_package.go @@ -2,40 +2,58 @@ package channeldb import ( "bytes" - "encoding/binary" "errors" - "fmt" - "io" + cstate "github.com/lightningnetwork/lnd/chanstate" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" ) -// 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") +type ( + // AddRef is used to identify a particular Add in a FwdPkg. + AddRef = cstate.AddRef -// FwdState is an enum used to describe the lifecycle of a FwdPkg. -type FwdState byte + // SettleFailRef is used to locate a Settle/Fail in another channel's + // FwdPkg. + SettleFailRef = cstate.SettleFailRef + + // 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 + + // 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. @@ -99,283 +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)) -} - -// 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/channeldb/revocation_log.go b/channeldb/revocation_log.go index 5a7f7a76bea..8340c8b19b1 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/channel.go b/chanstate/channel.go new file mode 100644 index 00000000000..0950f4c7294 --- /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 00000000000..b19fe3659ea --- /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/channel_type.go b/chanstate/channel_type.go new file mode 100644 index 00000000000..9666307bef1 --- /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 +} diff --git a/chanstate/close_summary.go b/chanstate/close_summary.go new file mode 100644 index 00000000000..779a4c638f4 --- /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/commitment.go b/chanstate/commitment.go new file mode 100644 index 00000000000..0cb49e02313 --- /dev/null +++ b/chanstate/commitment.go @@ -0,0 +1,304 @@ +package chanstate + +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" +) + +// 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, + 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) + } + clone.BlindingPoint = h.BlindingPoint + 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 +} + +// 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 +} + +// 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/commitment_test.go b/chanstate/commitment_test.go new file mode 100644 index 00000000000..ab1794d7712 --- /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], + ) +} diff --git a/chanstate/config.go b/chanstate/config.go new file mode 100644 index 00000000000..17e3e5e4fae --- /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/errors.go b/chanstate/errors.go new file mode 100644 index 00000000000..4e8415cf95a --- /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") +) diff --git a/chanstate/forwarding.go b/chanstate/forwarding.go new file mode 100644 index 00000000000..efee78e6ea5 --- /dev/null +++ b/chanstate/forwarding.go @@ -0,0 +1,307 @@ +package chanstate + +import ( + "bytes" + "encoding/binary" + "fmt" + "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 +} + +// 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)) +} diff --git a/chanstate/interface.go b/chanstate/interface.go index 49344855128..e19f4de84ec 100644 --- a/chanstate/interface.go +++ b/chanstate/interface.go @@ -1,11 +1,15 @@ package chanstate import ( + "net" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" - "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn/v2" "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. @@ -24,6 +28,28 @@ type Store interface { // HistoricalChannelStore owns the post-close historical channel view. HistoricalChannelStore + // OpenChannelLifecycleStore owns persisted lifecycle state for open + // channel records. + OpenChannelLifecycleStore + + // OpenChannelStatusStore owns persisted status flags for open channel + // records. + OpenChannelStatusStore + + // OpenChannelShutdownStore owns persisted shutdown state. + OpenChannelShutdownStore + + // OpenChannelCloseTxStore owns persisted closing transaction state. + OpenChannelCloseTxStore + + // OpenChannelCommitmentStore owns persisted commitment state for open + // channel records. + OpenChannelCommitmentStore + + // OpenChannelFwdPkgStore owns forwarding packages tied to open + // channel records. + OpenChannelFwdPkgStore + // ClosedChannelStore owns closed-channel summaries and lifecycle // mutations. ClosedChannelStore @@ -47,48 +73,46 @@ type OpenChannelStore interface { // 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) ([]*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) (*channeldb.OpenChannel, 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) (*channeldb.OpenChannel, 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() ([]*channeldb.OpenChannel, 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() ([]*channeldb.OpenChannel, 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() ([]*channeldb.OpenChannel, 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() ([]*channeldb.OpenChannel, 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 // 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,15 +120,193 @@ 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) error } // HistoricalChannelStore owns the post-close historical channel view. type HistoricalChannelStore interface { // FetchHistoricalChannel fetches open channel data from the // historical channel bucket. - FetchHistoricalChannel(outPoint *wire.OutPoint) ( - *channeldb.OpenChannel, error) + FetchHistoricalChannel(outPoint *wire.OutPoint) (*OpenChannel, error) +} + +// OpenChannelLifecycleStore owns persisted lifecycle state for open channel +// records. +type OpenChannelLifecycleStore interface { + // SyncPendingChannel writes a pending channel to the store and records + // the funding broadcast height. + 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 *OpenChannel) error + + // MarkChannelConfirmationHeight updates the channel's confirmation + // height once the channel opening transaction receives one + // confirmation. + 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 *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 *OpenChannel, + openLoc lnwire.ShortChannelID) error + + // MarkChannelRealScid marks the zero-conf channel's confirmed + // ShortChannelID. + 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 *OpenChannel) error +} + +// OpenChannelStatusStore owns persisted status flags for open channel records. +type OpenChannelStatusStore interface { + // ApplyChannelStatus adds the target status to the channel's + // persisted status bit field. + ApplyChannelStatus(channel *OpenChannel, status ChannelStatus) error + + // ClearChannelStatus clears the target status from the channel's + // persisted status bit field. + 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 *OpenChannel, + commitPoint *btcec.PublicKey) error + + // FetchChannelDataLossCommitPoint retrieves the commit point stored + // when the channel was marked as local-data-loss. + FetchChannelDataLossCommitPoint(channel *OpenChannel) ( + *btcec.PublicKey, error) + + // MarkChannelBorked marks the channel as irreconcilable. + MarkChannelBorked(channel *OpenChannel) error +} + +// OpenChannelShutdownStore owns persisted shutdown state. +type OpenChannelShutdownStore interface { + // StoreChannelShutdownInfo persists the ShutdownInfo for the target + // channel. + StoreChannelShutdownInfo(channel *OpenChannel, info *ShutdownInfo) error + + // FetchChannelShutdownInfo fetches the persisted ShutdownInfo for the + // target channel. + FetchChannelShutdownInfo(channel *OpenChannel) ( + fn.Option[ShutdownInfo], error) +} + +// OpenChannelCloseTxStore owns persisted closing transaction state. +type OpenChannelCloseTxStore interface { + // MarkChannelCommitmentBroadcasted marks the channel as having a + // commitment transaction broadcast. + MarkChannelCommitmentBroadcasted(channel *OpenChannel, + closeTx *wire.MsgTx, closer lntypes.ChannelParty) error + + // MarkChannelCoopBroadcasted marks the channel as having a + // cooperative close transaction broadcast. + MarkChannelCoopBroadcasted(channel *OpenChannel, closeTx *wire.MsgTx, + closer lntypes.ChannelParty) error + + // FetchChannelBroadcastedCommitment fetches the stored unilateral + // closing transaction. + FetchChannelBroadcastedCommitment(channel *OpenChannel) (*wire.MsgTx, + error) + + // FetchChannelBroadcastedCooperative fetches the stored cooperative + // closing transaction. + FetchChannelBroadcastedCooperative(channel *OpenChannel) (*wire.MsgTx, + error) +} + +// OpenChannelCommitmentStore owns persisted commitment state for open channel +// records. +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 *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 *OpenChannel, diff *CommitDiff) error + + // RemoteCommitChainTip returns the "tip" of the current remote + // commitment chain. + RemoteCommitChainTip(channel *OpenChannel) (*CommitDiff, error) + + // UnsignedAckedUpdates retrieves the persisted unsigned acked remote + // log updates that still need to be signed for. + UnsignedAckedUpdates(channel *OpenChannel) ([]LogUpdate, error) + + // RemoteUnsignedLocalUpdates retrieves the persisted, unsigned local + // log updates that the remote still needs to sign for. + RemoteUnsignedLocalUpdates(channel *OpenChannel) ([]LogUpdate, error) + + // InsertNextRevocation inserts the next commitment point into the + // persisted channel state. + 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 *OpenChannel, fwdPkg *FwdPkg, + updates []LogUpdate, ourOutputIndex, + theirOutputIndex uint32) error + + // CommitmentHeight returns the current persisted commitment height. + CommitmentHeight(channel *OpenChannel) (uint64, error) + + // LatestCommitments returns the two latest commitments for both the + // local and remote party. + 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 *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 *OpenChannel, updateNum uint64) ( + *RevocationLog, *ChannelCommitment, error) +} + +// OpenChannelFwdPkgStore owns forwarding packages tied to open channel records. +type OpenChannelFwdPkgStore interface { + // LoadFwdPkgs loads forwarding packages that have not been processed. + LoadFwdPkgs(channel *OpenChannel) ([]*FwdPkg, error) + + // AckAddHtlcs marks add HTLCs in forwarding packages as resolved. + AckAddHtlcs(channel *OpenChannel, addRefs ...AddRef) error + + // AckSettleFails marks settles or fails as delivered to the incoming + // link. + AckSettleFails(channel *OpenChannel, + settleFailRefs ...SettleFailRef) error + + // SetFwdFilter writes the forwarding filter for the forwarding package + // identified by height. + SetFwdFilter(channel *OpenChannel, height uint64, + fwdFilter *PkgFilter) error + + // RemoveFwdPkgs removes forwarding packages by remote commitment + // height. + RemoveFwdPkgs(channel *OpenChannel, heights ...uint64) error } // ClosedChannelStore owns closed-channel summaries and lifecycle mutations. @@ -117,17 +319,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 +344,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 *OpenChannel, 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 +360,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 +412,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.go b/chanstate/open_channel.go new file mode 100644 index 00000000000..4bf293c5137 --- /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 + + // 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 +} diff --git a/chanstate/open_channel_types.go b/chanstate/open_channel_types.go new file mode 100644 index 00000000000..90ab06bc629 --- /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 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 +} diff --git a/chanstate/revocation_log.go b/chanstate/revocation_log.go new file mode 100644 index 00000000000..712b8ae9ac4 --- /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) +} diff --git a/chanstate/shutdown.go b/chanstate/shutdown.go new file mode 100644 index 00000000000..4c1dca31a51 --- /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 +} diff --git a/chanstate/snapshot.go b/chanstate/snapshot.go new file mode 100644 index 00000000000..430740beb87 --- /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 +} diff --git a/chanstate/taproot.go b/chanstate/taproot.go new file mode 100644 index 00000000000..3bb575a0801 --- /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) +} diff --git a/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go index 869a0093e01..2d07e646c61 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 { @@ -2320,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{ @@ -2338,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/contractcourt/utils_test.go b/contractcourt/utils_test.go index 994bc57a88c..22c62217ea3 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 29b4f902d0b..ccc8f61d12b 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" @@ -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) @@ -5769,42 +5769,14 @@ func TestChannelLinkCleanupSpuriousResponses(t *testing.T) { } } -type mockPackager struct { - failLoadFwdPkgs bool +type mockFailLoadFwdPkgStore struct { + cstate.Store } -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 2e084250943..796a2506410 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 @@ -306,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, } @@ -325,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 { @@ -954,9 +965,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 +1244,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() diff --git a/lnwallet/taproot_test_vectors_test.go b/lnwallet/taproot_test_vectors_test.go index 7e3dcf8e6a0..1c6e26fa81b 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 738558e224f..dec99d8941a 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 38131eaa724..d740565c1e5 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 670af094605..7aa4c96fa54 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.