Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
440 changes: 405 additions & 35 deletions contractcourt/breach_arbitrator.go

Large diffs are not rendered by default.

679 changes: 677 additions & 2 deletions contractcourt/breach_arbitrator_test.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions contractcourt/chain_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,7 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error
extractStateNumHint: lnwallet.GetStateNumHint,
auxLeafStore: c.cfg.AuxLeafStore,
auxResolver: c.cfg.AuxResolver,
auxSigner: c.cfg.AuxSigner,
auxCloser: c.cfg.AuxCloser,
chanCloseConfs: c.cfg.ChannelCloseConfs,
},
Expand Down Expand Up @@ -1327,6 +1328,7 @@ func (c *ChainArbitrator) loadOpenChannels() error {
extractStateNumHint: lnwallet.GetStateNumHint,
auxLeafStore: c.cfg.AuxLeafStore,
auxResolver: c.cfg.AuxResolver,
auxSigner: c.cfg.AuxSigner,
auxCloser: c.cfg.AuxCloser,
chanCloseConfs: c.cfg.ChannelCloseConfs,
},
Expand Down
8 changes: 7 additions & 1 deletion contractcourt/chain_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ type chainWatcherConfig struct {
// auxResolver is used to supplement contract resolution.
auxResolver fn.Option[lnwallet.AuxContractResolver]

// auxSigner is an optional signer that can be used to determine
// channel-specific HTLC sighash types based on negotiated features.
auxSigner fn.Option[lnwallet.AuxSigner]

// auxCloser is used to finalize cooperative closes.
auxCloser fn.Option[AuxChanCloser]

Expand Down Expand Up @@ -1072,6 +1076,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail,
retribution, err := lnwallet.NewBreachRetribution(
c.cfg.chanState, broadcastStateNum, spendHeight,
commitSpend.SpendingTx, c.cfg.auxLeafStore, c.cfg.auxResolver,
c.cfg.auxSigner,
)

switch {
Expand Down Expand Up @@ -1415,7 +1420,7 @@ func (c *chainWatcher) dispatchLocalForceClose(
forceClose, err := lnwallet.NewLocalForceCloseSummary(
c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx,
uint32(commitSpend.SpendingHeight), stateNum,
c.cfg.auxLeafStore, c.cfg.auxResolver,
c.cfg.auxLeafStore, c.cfg.auxResolver, c.cfg.auxSigner,
)
if err != nil {
return err
Expand Down Expand Up @@ -1522,6 +1527,7 @@ func (c *chainWatcher) dispatchRemoteForceClose(
uniClose, err := lnwallet.NewUnilateralCloseSummary(
c.cfg.chanState, c.cfg.signer, commitSpend, remoteCommit,
commitPoint, c.cfg.auxLeafStore, c.cfg.auxResolver,
c.cfg.auxSigner,
)
if err != nil {
return err
Expand Down
41 changes: 41 additions & 0 deletions contractcourt/htlc_success_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,39 @@ func (h *htlcSuccessResolver) isZeroFeeOutput() bool {
h.htlcResolution.SignDetails != nil
}

// isSigHashDefault returns true when the second-level HTLC transaction was
// signed with SigHashDefault. In this case the pre-signed transaction has
// baked-in fees and must be broadcast as-is — the sweeper cannot add wallet
// inputs or change outputs without invalidating the peer's signature.
//
// NOTE: This only applies to taproot-assets (custom channel) HTLCs, so we
// also require a resolution blob to be present (which is only set for aux
// channels). This prevents accidental activation for regular lnd channels
// where the zero-value SigHashType (0x00) would otherwise match.
func (h *htlcSuccessResolver) isSigHashDefault() bool {
sd := h.htlcResolution.SignDetails

return sd != nil &&
sd.SigHashType == txscript.SigHashDefault &&
h.htlcResolution.ResolutionBlob.IsSome()
}

// publishSuccessTx directly broadcasts the pre-signed second-level HTLC
// success transaction. This is used when the transaction was signed with
// SigHashDefault (baked-in fees), where the sweeper's normal tx-rebuilding
// flow would invalidate the peer's signature.
func (h *htlcSuccessResolver) publishSuccessTx() error {
h.log.Infof("publishing pre-signed 2nd-level HTLC success tx=%v "+
"(SigHashDefault, baked-in fees)",
h.htlcResolution.SignedSuccessTx.TxHash())

label := labels.MakeLabel(
labels.LabelTypeChannelClose, &h.ShortChanID,
)

return h.PublishTx(h.htlcResolution.SignedSuccessTx, label)
}

// isTaproot returns true if the resolver is for a taproot output.
func (h *htlcSuccessResolver) isTaproot() bool {
return txscript.IsPayToTaproot(
Expand Down Expand Up @@ -759,6 +792,14 @@ func (h *htlcSuccessResolver) Launch() error {
return h.sweepSuccessTxOutput()
}

// When the peer signed with SigHashDefault the pre-signed
// second-level tx has baked-in fees and cannot be modified
// (adding wallet inputs would invalidate the signature).
// Publish it directly instead of going through the sweeper.
if h.isSigHashDefault() {
return h.publishSuccessTx()
}

// Otherwise, sweep the second level tx.
return h.sweepSuccessTx()

Expand Down
42 changes: 42 additions & 0 deletions contractcourt/htlc_timeout_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/labels"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
Expand Down Expand Up @@ -946,6 +947,39 @@ func (h *htlcTimeoutResolver) isZeroFeeOutput() bool {
h.htlcResolution.SignDetails != nil
}

// isSigHashDefault returns true when the second-level HTLC transaction was
// signed with SigHashDefault. In this case the pre-signed transaction has
// baked-in fees and must be broadcast as-is — the sweeper cannot add wallet
// inputs or change outputs without invalidating the peer's signature.
//
// NOTE: This only applies to taproot-assets (custom channel) HTLCs, so we
// also require a resolution blob to be present (which is only set for aux
// channels). This prevents accidental activation for regular lnd channels
// where the zero-value SigHashType (0x00) would otherwise match.
func (h *htlcTimeoutResolver) isSigHashDefault() bool {
sd := h.htlcResolution.SignDetails

return sd != nil &&
sd.SigHashType == txscript.SigHashDefault &&
h.htlcResolution.ResolutionBlob.IsSome()
}

// publishTimeoutTx directly broadcasts the pre-signed second-level HTLC
// timeout transaction. This is used when the transaction was signed with
// SigHashDefault (baked-in fees), where the sweeper's normal tx-rebuilding
// flow would invalidate the peer's signature.
func (h *htlcTimeoutResolver) publishTimeoutTx() error {
h.log.Infof("publishing pre-signed 2nd-level HTLC timeout tx=%v "+
"(SigHashDefault, baked-in fees)",
h.htlcResolution.SignedTimeoutTx.TxHash())

label := labels.MakeLabel(
labels.LabelTypeChannelClose, &h.ShortChanID,
)

return h.PublishTx(h.htlcResolution.SignedTimeoutTx, label)
}

// waitHtlcSpendAndCheckPreimage waits for the htlc output to be spent and
// checks whether the spending reveals the preimage. If the preimage is found,
// it will be added to the preimage beacon to settle the incoming link, and a
Expand Down Expand Up @@ -1291,6 +1325,14 @@ func (h *htlcTimeoutResolver) Launch() error {
return h.sweepTimeoutTxOutput()
}

// When the peer signed with SigHashDefault the pre-signed
// second-level tx has baked-in fees and cannot be modified
// (adding wallet inputs would invalidate the signature).
// Publish it directly instead of going through the sweeper.
if h.isSigHashDefault() {
return h.publishTimeoutTx()
}

// Otherwise, sweep the second level tx.
return h.sweepTimeoutTx()

Expand Down
7 changes: 4 additions & 3 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -2184,7 +2184,8 @@ func (l *channelLink) getDustClosure() dustClosure {
remoteDustLimit := l.channel.State().RemoteChanCfg.DustLimit
chanType := l.channel.State().ChanType

return dustHelper(chanType, localDustLimit, remoteDustLimit)
return dustHelper(chanType, localDustLimit, remoteDustLimit,
l.channel.IsChanSigHashDefault())
}

// getCommitFee returns either the local or remote CommitFee in satoshis. This
Expand Down Expand Up @@ -2352,7 +2353,7 @@ type dustClosure func(feerate chainfee.SatPerKWeight, incoming bool,

// dustHelper is used to construct the dustClosure.
func dustHelper(chantype channeldb.ChannelType, localDustLimit,
remoteDustLimit btcutil.Amount) dustClosure {
remoteDustLimit btcutil.Amount, sigHashDefault bool) dustClosure {

isDust := func(feerate chainfee.SatPerKWeight, incoming bool,
whoseCommit lntypes.ChannelParty, amt btcutil.Amount) bool {
Expand All @@ -2366,7 +2367,7 @@ func dustHelper(chantype channeldb.ChannelType, localDustLimit,

return lnwallet.HtlcIsDust(
chantype, incoming, whoseCommit, feerate, amt,
dustLimit,
dustLimit, sigHashDefault,
)
}

Expand Down
2 changes: 1 addition & 1 deletion htlcswitch/mailbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ func testMailBoxDust(t *testing.T, chantype channeldb.ChannelType) {

localDustLimit := btcutil.Amount(400)
remoteDustLimit := btcutil.Amount(500)
isDust := dustHelper(chantype, localDustLimit, remoteDustLimit)
isDust := dustHelper(chantype, localDustLimit, remoteDustLimit, false)
ctx.mailbox.SetDustClosure(isDust)

// The first packet will be dust according to the remote dust limit,
Expand Down
1 change: 1 addition & 0 deletions htlcswitch/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,7 @@ func (f *mockChannelLink) getDustClosure() dustClosure {
dustLimit := btcutil.Amount(400)
return dustHelper(
channeldb.SingleFunderTweaklessBit, dustLimit, dustLimit,
false,
)
}

Expand Down
11 changes: 0 additions & 11 deletions input/signdescriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package input

import (
"encoding/binary"
"errors"
"fmt"
"io"

Expand All @@ -12,11 +11,6 @@ import (
"github.com/lightningnetwork/lnd/keychain"
)

var (
// ErrTweakOverdose signals a SignDescriptor is invalid because both of its
// SingleTweak and DoubleTweak are non-nil.
ErrTweakOverdose = errors.New("sign descriptor should only have one tweak")
)

// SignDescriptor houses the necessary information required to successfully
// sign a given segwit output. This struct is used by the Signer interface in
Expand Down Expand Up @@ -289,11 +283,6 @@ func ReadSignDescriptor(r io.Reader, sd *SignDescriptor) error {
sd.DoubleTweak, _ = btcec.PrivKeyFromBytes(doubleTweakBytes)
}

// Only one tweak should ever be set, fail if both are present.
if sd.SingleTweak != nil && sd.DoubleTweak != nil {
return ErrTweakOverdose
}

witnessScript, err := wire.ReadVarBytes(r, 0, 500, "witnessScript")
if err != nil {
return err
Expand Down
4 changes: 3 additions & 1 deletion input/size_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1490,7 +1490,9 @@ func genSuccessTx(t *testing.T, chanType channeldb.ChannelType) *wire.MsgTx {
},
}

sigHashType := lnwallet.HtlcSigHashType(channeldb.SingleFunderBit)
sigHashType := lnwallet.HtlcSigHashType(
channeldb.SingleFunderBit,
)

var successWitness [][]byte

Expand Down
8 changes: 6 additions & 2 deletions lnwallet/aux_leaf_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,12 @@ type AuxLeafStore interface {

// FetchLeavesFromRevocation attempts to fetch the auxiliary leaves
// from a channel revocation that stores balance + blob information.
FetchLeavesFromRevocation(
r *channeldb.RevocationLog) fn.Result[CommitDiffAuxResult]
// The additional parameters (chanState, keys, commitTx) are needed
// to compute second-level HTLC auxiliary leaves at runtime, since
// these are not stored in the commitment blob.
FetchLeavesFromRevocation(r *channeldb.RevocationLog,
chanState AuxChanState, keys CommitmentKeyRing,
commitTx *wire.MsgTx) fn.Result[CommitDiffAuxResult]

// ApplyHtlcView serves as the state transition function for the custom
// channel's blob. Given the old blob, and an HTLC view, then a new
Expand Down
10 changes: 10 additions & 0 deletions lnwallet/aux_resolutions.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ type ResolutionReq struct {
// AuxSigDesc is an optional field that contains additional information
// needed to sweep second level HTLCs.
AuxSigDesc fn.Option[AuxSigDesc]

// SecondLevelTx is the second-level HTLC transaction, set only when
// resolving a TaprootHtlcSecondLevelRevoke. This allows the resolver
// to re-anchor proofs to the second-level tx rather than the
// commitment tx.
SecondLevelTx *wire.MsgTx

// SecondLevelTxBlockHeight is the block height where the second-level
// HTLC transaction was confirmed.
SecondLevelTxBlockHeight uint32
}

// AuxContractResolver is an interface that is used to resolve contracts that
Expand Down
59 changes: 59 additions & 0 deletions lnwallet/aux_signer.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package lnwallet

import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
Expand Down Expand Up @@ -305,4 +307,61 @@ type AuxSigner interface {
// sig jobs.
VerifySecondLevelSigs(chanState AuxChanState, commitTx *wire.MsgTx,
verifyJob []AuxVerifyJob) error

// HtlcSigHashType returns the sighash type to use for HTLC
// second-level transactions for the given channel. The caller
// populates HtlcSigHashReq with either a ChanID (for live
// feature-negotiation lookups on new commitments) or a CommitBlob
// (for existing commitments where the blob is the source of truth),
// or both. The implementation decides the lookup strategy.
HtlcSigHashType(
req HtlcSigHashReq,
) fn.Option[txscript.SigHashType]
}

// HtlcSigHashReq is the request passed to AuxSigner.HtlcSigHashType.
// Callers populate either ChanID (for next-commitment signing/verification
// where live feature negotiation is authoritative) or CommitBlob (for
// existing commitments where the blob records what was actually used), or
// both.
type HtlcSigHashReq struct {
// ChanID identifies the channel for live feature-negotiation lookups.
// Set when determining the sighash for a new commitment being
// signed or verified.
ChanID fn.Option[lnwire.ChannelID]

// CommitBlob is the commitment custom blob that may contain a cached
// SigHashDefault flag. Set when resolving the sighash for an
// already-persisted commitment (breach, resolution).
CommitBlob fn.Option[tlv.Blob]
}

// ResolveHtlcSigHashType determines the sighash type to use for HTLC
// second-level transactions. It queries the aux signer (if present) with the
// given request. If the aux signer returns None (or is not present), it falls
// back to the default HtlcSigHashType based on channel type.
func ResolveHtlcSigHashType(chanType channeldb.ChannelType,
auxSigner fn.Option[AuxSigner],
req HtlcSigHashReq) txscript.SigHashType {

sigHash := fn.FlatMapOption(
func(s AuxSigner) fn.Option[txscript.SigHashType] {
return s.HtlcSigHashType(req)
},
)(auxSigner)

return sigHash.UnwrapOr(HtlcSigHashType(chanType))
}

// IsSigHashDefault returns true if the resolved HTLC sighash type for the
// given channel is SigHashDefault. This is used to determine whether
// second-level HTLC transactions must carry their own fee (since the sweeper
// cannot add wallet inputs under SigHashDefault).
func IsSigHashDefault(chanType channeldb.ChannelType,
auxSigner fn.Option[AuxSigner],
req HtlcSigHashReq) bool {

return ResolveHtlcSigHashType(
chanType, auxSigner, req,
) == txscript.SigHashDefault
}
Loading
Loading