Skip to content

Commit 61e9e2d

Browse files
committed
lnwallet: update internal co-op close flow to support musig2 keyspend
In this commit, we update the co-op close flow to support the new musig2 keyspend flow. We'll use some new functional options to allow a caller to pass in an active musig2 session. If this is present, then we'll use that to complete the musig2 flow by signing with a partial signature, and then ultimately combining the signatures at the end.
1 parent 7e0ea47 commit 61e9e2d

File tree

1 file changed

+117
-28
lines changed

1 file changed

+117
-28
lines changed

lnwallet/channel.go

Lines changed: 117 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7039,6 +7039,30 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
70397039
}, nil
70407040
}
70417041

7042+
// chanCloseOpt is a functional option that can be used to modify the co-op
7043+
// close process.
7044+
type chanCloseOpt struct {
7045+
musigSession *MusigSession
7046+
}
7047+
7048+
// ChanCloseOpt is a closure type that cen be used to modify the set of default
7049+
// options.
7050+
type ChanCloseOpt func(*chanCloseOpt)
7051+
7052+
// defaultCloseOpts is the default set of close options.
7053+
func defaultCloseOpts() *chanCloseOpt {
7054+
return &chanCloseOpt{}
7055+
}
7056+
7057+
// WithCoopCloseMusigSession can be used to apply an existing musig2 session to
7058+
// the cooperative close process. If specified, then a musig2 co-op close
7059+
// (single sig keyspend) will be used.
7060+
func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt {
7061+
return func(opts *chanCloseOpt) {
7062+
opts.musigSession = session
7063+
}
7064+
}
7065+
70427066
// CreateCloseProposal is used by both parties in a cooperative channel close
70437067
// workflow to generate proposed close transactions and signatures. This method
70447068
// should only be executed once all pending HTLCs (if any) on the channel have
@@ -7050,8 +7074,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
70507074
// TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs,
70517075
// settle any in flight.
70527076
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
7053-
localDeliveryScript []byte,
7054-
remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash,
7077+
localDeliveryScript []byte, remoteDeliveryScript []byte,
7078+
closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
70557079
btcutil.Amount, error) {
70567080

70577081
lc.Lock()
@@ -7063,6 +7087,11 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
70637087
return nil, nil, 0, ErrChanClosing
70647088
}
70657089

7090+
opts := defaultCloseOpts()
7091+
for _, optFunc := range closeOpts {
7092+
optFunc(opts)
7093+
}
7094+
70667095
// Get the final balances after subtracting the proposed fee, taking
70677096
// care not to persist the adjusted balance, as the feeRate may change
70687097
// during the channel closing process.
@@ -7088,14 +7117,25 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
70887117
return nil, nil, 0, err
70897118
}
70907119

7091-
// Finally, sign the completed cooperative closure transaction. As the
7092-
// initiator we'll simply send our signature over to the remote party,
7093-
// using the generated txid to be notified once the closure transaction
7094-
// has been confirmed.
7095-
lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx)
7096-
sig, err := lc.Signer.SignOutputRaw(closeTx, lc.signDesc)
7097-
if err != nil {
7098-
return nil, nil, 0, err
7120+
// If we have a co-op close musig session, then this is a taproot
7121+
// channel, so we'll generate a _partial_ signature.
7122+
var sig input.Signature
7123+
if opts.musigSession != nil {
7124+
sig, err = opts.musigSession.SignCommit(closeTx)
7125+
if err != nil {
7126+
return nil, nil, 0, err
7127+
}
7128+
} else {
7129+
// For regular channels we'll, sign the completed cooperative
7130+
// closure transaction. As the initiator we'll simply send our
7131+
// signature over to the remote party, using the generated txid
7132+
// to be notified once the closure transaction has been
7133+
// confirmed.
7134+
lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx)
7135+
sig, err = lc.Signer.SignOutputRaw(closeTx, lc.signDesc)
7136+
if err != nil {
7137+
return nil, nil, 0, err
7138+
}
70997139
}
71007140

71017141
// As everything checks out, indicate in the channel status that a
@@ -7116,7 +7156,8 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
71167156
func (lc *LightningChannel) CompleteCooperativeClose(
71177157
localSig, remoteSig input.Signature,
71187158
localDeliveryScript, remoteDeliveryScript []byte,
7119-
proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) {
7159+
proposedFee btcutil.Amount,
7160+
closeOpts ...ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) {
71207161

71217162
lc.Lock()
71227163
defer lc.Unlock()
@@ -7127,6 +7168,11 @@ func (lc *LightningChannel) CompleteCooperativeClose(
71277168
return nil, 0, ErrChanClosing
71287169
}
71297170

7171+
opts := defaultCloseOpts()
7172+
for _, optFunc := range closeOpts {
7173+
optFunc(opts)
7174+
}
7175+
71307176
// Get the final balances after subtracting the proposed fee.
71317177
ourBalance, theirBalance, err := CoopCloseBalance(
71327178
lc.channelState.ChanType, lc.channelState.IsInitiator,
@@ -7149,31 +7195,62 @@ func (lc *LightningChannel) CompleteCooperativeClose(
71497195
// consensus rules such as being too big, or having any value with a
71507196
// negative output.
71517197
tx := btcutil.NewTx(closeTx)
7198+
prevOut := lc.signDesc.Output
71527199
if err := blockchain.CheckTransactionSanity(tx); err != nil {
71537200
return nil, 0, err
71547201
}
7155-
hashCache := input.NewTxSigHashesV0Only(closeTx)
7156-
7157-
// Finally, construct the witness stack minding the order of the
7158-
// pubkeys+sigs on the stack.
7159-
ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
7160-
SerializeCompressed()
7161-
theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
7162-
SerializeCompressed()
7163-
witness := input.SpendMultiSig(
7164-
lc.signDesc.WitnessScript, ourKey, localSig, theirKey,
7165-
remoteSig,
7202+
7203+
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
7204+
prevOut.PkScript, prevOut.Value,
71667205
)
7167-
closeTx.TxIn[0].Witness = witness
7206+
hashCache := txscript.NewTxSigHashes(closeTx, prevOutputFetcher)
7207+
7208+
// Next, we'll complete the co-op close transaction. Depending on the
7209+
// set of options, we'll either do a regular p2wsh spend, or construct
7210+
// the final schnorr signature from a set of partial sigs.
7211+
if opts.musigSession != nil {
7212+
// For taproot channels, we'll use the attached session to
7213+
// combine the two partial signatures into a proper schnorr
7214+
// signature.
7215+
remotePartialSig, ok := remoteSig.(*MusigPartialSig)
7216+
if !ok {
7217+
return nil, 0, fmt.Errorf("expected MusigPartialSig, "+
7218+
"got %T", remoteSig)
7219+
}
7220+
7221+
finalSchnorrSig, err := opts.musigSession.CombineSigs(
7222+
remotePartialSig.sig,
7223+
)
7224+
if err != nil {
7225+
return nil, 0, fmt.Errorf("unable to combine "+
7226+
"final co-op close sig: %w", err)
7227+
}
7228+
7229+
// The witness for a keyspend is just the signature itself.
7230+
closeTx.TxIn[0].Witness = wire.TxWitness{
7231+
finalSchnorrSig.Serialize(),
7232+
}
7233+
} else {
7234+
7235+
// For regular channels, we'll need to , construct the witness
7236+
// stack minding the order of the pubkeys+sigs on the stack.
7237+
ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
7238+
SerializeCompressed()
7239+
theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
7240+
SerializeCompressed()
7241+
witness := input.SpendMultiSig(
7242+
lc.signDesc.WitnessScript, ourKey, localSig, theirKey,
7243+
remoteSig,
7244+
)
7245+
closeTx.TxIn[0].Witness = witness
7246+
7247+
}
71687248

71697249
// Validate the finalized transaction to ensure the output script is
71707250
// properly met, and that the remote peer supplied a valid signature.
7171-
prevOut := lc.signDesc.Output
71727251
vm, err := txscript.NewEngine(
71737252
prevOut.PkScript, closeTx, 0, txscript.StandardVerifyFlags, nil,
7174-
hashCache, prevOut.Value, txscript.NewCannedPrevOutputFetcher(
7175-
prevOut.PkScript, prevOut.Value,
7176-
),
7253+
hashCache, prevOut.Value, prevOutputFetcher,
71777254
)
71787255
if err != nil {
71797256
return nil, 0, err
@@ -7833,11 +7910,23 @@ func (lc *LightningChannel) IdealCommitFeeRate(netFeeRate, minRelayFeeRate,
78337910
return absoluteMaxFee
78347911
}
78357912

7836-
// RemoteNextRevocation returns the channelState's RemoteNextRevocation.
7913+
// RemoteNextRevocation returns the channelState's RemoteNextRevocation. For
7914+
// musig2 channels, until a nonce pair is processed by the remote party, a nil
7915+
// public key is returned.
7916+
//
7917+
// TODO(roasbeef): revisit, maybe just make a more general method instead?
78377918
func (lc *LightningChannel) RemoteNextRevocation() *btcec.PublicKey {
78387919
lc.RLock()
78397920
defer lc.RUnlock()
78407921

7922+
if !lc.channelState.ChanType.IsTaproot() {
7923+
return lc.channelState.RemoteNextRevocation
7924+
}
7925+
7926+
if lc.musigSessions == nil {
7927+
return nil
7928+
}
7929+
78417930
return lc.channelState.RemoteNextRevocation
78427931
}
78437932

0 commit comments

Comments
 (0)