diff --git a/lnwallet/channel.go b/lnwallet/channel.go index fb1e3ac4b..02f5f9ccf 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -833,6 +833,20 @@ type LightningChannel struct { // is created. type ChannelOpt func(*channelOpts) +// AuxHtlcValidator is an interface for validating whether an HTLC can be added +// to a custom channel. It is called during HTLC validation with the current +// channel state and HTLC details. This allows external components (like the +// traffic shaper) to perform final validation checks against the most +// up-to-date channel state before the HTLC is committed. +type AuxHtlcValidator interface { + // ValidateHtlc checks whether the given HTLC can be added to the + // channel given the current link bandwidth, custom records, and HTLC + // view. + ValidateHtlc(amount, linkBandwidth lnwire.MilliSatoshi, + customRecords lnwire.CustomRecords, + view AuxHtlcView) error +} + // channelOpts is the set of options used to create a new channel. type channelOpts struct { localNonce *musig2.Nonces @@ -842,6 +856,10 @@ type channelOpts struct { auxSigner fn.Option[AuxSigner] auxResolver fn.Option[AuxContractResolver] + // auxHtlcValidator is an optional validator that performs custom + // validation on HTLCs before they are added to the channel state. + auxHtlcValidator fn.Option[AuxHtlcValidator] + skipNonceInit bool } @@ -894,6 +912,15 @@ func WithAuxResolver(resolver AuxContractResolver) ChannelOpt { } } +// WithAuxHtlcValidator is used to specify a custom HTLC validator for the +// channel. This validator will be called during HTLC addition to perform +// final validation checks against the most up-to-date channel state. +func WithAuxHtlcValidator(validator AuxHtlcValidator) ChannelOpt { + return func(o *channelOpts) { + o.auxHtlcValidator = fn.Some(validator) + } +} + // defaultChannelOpts returns the set of default options for a new channel. func defaultChannelOpts() *channelOpts { return &channelOpts{} @@ -2738,9 +2765,21 @@ func (lc *LightningChannel) FetchLatestAuxHTLCView() AuxHtlcView { lc.RLock() defer lc.RUnlock() - return newAuxHtlcView(lc.fetchHTLCView( - lc.updateLogs.Remote.logIndex, lc.updateLogs.Local.logIndex, - )) + nextHeight := lc.commitChains.Local.tip().height + 1 + + // We use the remote ACKed index from the last signed local commitment + // (tail) rather than the remote's latest log index. This ensures we + // only include remote HTLCs that have been locked into a signed + // commitment, giving the aux validator a stable, consistent view that + // matches the actual commitment state used for balance calculations. + remoteACKedIndex := lc.commitChains.Local.tail().messageIndices.Remote + view := lc.fetchHTLCView( + remoteACKedIndex, lc.updateLogs.Local.logIndex, + ) + + view.NextHeight = nextHeight + + return newAuxHtlcView(view) } // fetchHTLCView returns all the candidate HTLC updates which should be @@ -6065,6 +6104,52 @@ func (lc *LightningChannel) addHTLC(htlc *lnwire.UpdateAddHTLC, return 0, err } + // If an auxiliary HTLC validator is configured, call it now to perform + // custom validation checks against the current channel state. This is + // the final validation point before the HTLC is added to the update + // log, ensuring that the validator sees the most up-to-date state + // including all previously validated HTLCs in this batch. + // + // NOTE: This is called after the standard commitment sanity checks to + // ensure we only perform (potentially) expensive custom validation on + // HTLCs that have already passed the basic Lightning protocol + // constraints. + err := fn.MapOptionZ( + lc.opts.auxHtlcValidator, + func(validator AuxHtlcValidator) error { + // Fetch the current HTLC view which includes all + // pending HTLCs that haven't been committed yet. This + // provides the validator with the most accurate state. + commitChain := lc.commitChains.Local + remoteIndex := commitChain.tail().messageIndices.Remote + view := lc.fetchHTLCView( + remoteIndex, lc.updateLogs.Local.logIndex, + ) + + nextHeight := lc.commitChains.Local.tip().height + 1 + view.NextHeight = nextHeight + + lc.log.Debugf("Setting view nextheight=%v", nextHeight) + + auxView := newAuxHtlcView(view) + + // Get the current available balance for the link + // bandwidth check. This is needed for the balance + // validation in the traffic shaper. We use NoBuffer + // since the buffer check was already performed earlier, + // and assets don't pay on-chain fees. + linkBandwidth, _ := lc.availableBalance(NoBuffer) + + return validator.ValidateHtlc( + pd.Amount, linkBandwidth, pd.CustomRecords, + auxView, + ) + }, + ) + if err != nil { + return 0, fmt.Errorf("aux HTLC validation failed: %w", err) + } + lc.updateLogs.Local.appendHtlc(pd) return pd.HtlcIndex, nil @@ -6216,6 +6301,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC, // remote commitments. func (lc *LightningChannel) validateAddHtlc(pd *paymentDescriptor, buffer BufferType) error { + // Make sure adding this HTLC won't violate any of the constraints we // must keep on the commitment transactions. remoteACKedIndex := lc.commitChains.Local.tail().messageIndices.Remote