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.
This commit is contained in:
6 files changed
+816
-150
No files matched your search
+221
-121
@@ -4,9 +4,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -94,75 +94,16 @@ const (
|
|||||||
defaultMaxFeeMultiplier = 3
|
defaultMaxFeeMultiplier = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// Channel abstracts away from the core channel state machine by exposing an
|
|
||||||
// interface that requires only the methods we need to carry out the channel
|
|
||||||
// closing process.
|
|
||||||
type Channel interface {
|
|
||||||
// ChannelPoint returns the channel point of the target channel.
|
|
||||||
ChannelPoint() *wire.OutPoint
|
|
||||||
|
|
||||||
// MarkCoopBroadcasted persistently marks that the channel close
|
|
||||||
// transaction has been broadcast.
|
|
||||||
MarkCoopBroadcasted(*wire.MsgTx, bool) error
|
|
||||||
|
|
||||||
// IsInitiator returns true we are the initiator of the channel.
|
|
||||||
IsInitiator() bool
|
|
||||||
|
|
||||||
// ShortChanID returns the scid of the channel.
|
|
||||||
ShortChanID() lnwire.ShortChannelID
|
|
||||||
|
|
||||||
// AbsoluteThawHeight returns the absolute thaw height of the channel.
|
|
||||||
// If the channel is pending, or an unconfirmed zero conf channel, then
|
|
||||||
// an error should be returned.
|
|
||||||
AbsoluteThawHeight() (uint32, error)
|
|
||||||
|
|
||||||
// LocalBalanceDust returns true if when creating a co-op close
|
|
||||||
// transaction, the balance of the local party will be dust after
|
|
||||||
// accounting for any anchor outputs.
|
|
||||||
LocalBalanceDust() bool
|
|
||||||
|
|
||||||
// RemoteBalanceDust returns true if when creating a co-op close
|
|
||||||
// transaction, the balance of the remote party will be dust after
|
|
||||||
// accounting for any anchor outputs.
|
|
||||||
RemoteBalanceDust() bool
|
|
||||||
|
|
||||||
// RemoteUpfrontShutdownScript returns the upfront shutdown script of
|
|
||||||
// the remote party. If the remote party didn't specify such a script,
|
|
||||||
// an empty delivery address should be returned.
|
|
||||||
RemoteUpfrontShutdownScript() lnwire.DeliveryAddress
|
|
||||||
|
|
||||||
// CreateCloseProposal creates a new co-op close proposal in the form
|
|
||||||
// of a valid signature, the chainhash of the final txid, and our final
|
|
||||||
// balance in the created state.
|
|
||||||
CreateCloseProposal(proposedFee btcutil.Amount, localDeliveryScript []byte,
|
|
||||||
remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash,
|
|
||||||
btcutil.Amount, error)
|
|
||||||
|
|
||||||
// CompleteCooperativeClose persistently "completes" the cooperative
|
|
||||||
// close by producing a fully signed co-op close transaction.
|
|
||||||
CompleteCooperativeClose(localSig, remoteSig input.Signature,
|
|
||||||
localDeliveryScript, remoteDeliveryScript []byte,
|
|
||||||
proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CoopFeeEstimator is used to estimate the fee of a co-op close transaction.
|
|
||||||
type CoopFeeEstimator interface {
|
|
||||||
// EstimateFee estimates an _absolute_ fee for a co-op close transaction
|
|
||||||
// given the local+remote tx outs (for the co-op close transaction),
|
|
||||||
// channel type, and ideal fee rate. If a passed TxOut is nil, then
|
|
||||||
// that indicates that an output is dust on the co-op close transaction
|
|
||||||
// _before_ fees are accounted for.
|
|
||||||
EstimateFee(chanType channeldb.ChannelType,
|
|
||||||
localTxOut, remoteTxOut *wire.TxOut,
|
|
||||||
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChanCloseCfg holds all the items that a ChanCloser requires to carry out its
|
// ChanCloseCfg holds all the items that a ChanCloser requires to carry out its
|
||||||
// duties.
|
// duties.
|
||||||
type ChanCloseCfg struct {
|
type ChanCloseCfg struct {
|
||||||
// Channel is the channel that should be closed.
|
// Channel is the channel that should be closed.
|
||||||
Channel Channel
|
Channel Channel
|
||||||
|
|
||||||
|
// MusigSession is used to handle generating musig2 nonces, and also
|
||||||
|
// creating the proper set of closing options for taproot channels.
|
||||||
|
MusigSession MusigSession
|
||||||
|
|
||||||
// BroadcastTx broadcasts the passed transaction to the network.
|
// BroadcastTx broadcasts the passed transaction to the network.
|
||||||
BroadcastTx func(*wire.MsgTx, string) error
|
BroadcastTx func(*wire.MsgTx, string) error
|
||||||
|
|
||||||
@@ -367,19 +308,35 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) {
|
|||||||
// closing script.
|
// closing script.
|
||||||
shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)
|
shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)
|
||||||
|
|
||||||
// Before closing, we'll attempt to send a disable update for the channel.
|
// If this is a taproot channel, then we'll need to also generate a
|
||||||
// We do so before closing the channel as otherwise the current edge policy
|
// nonce that'll be used sign the co-op close transaction offer.
|
||||||
// won't be retrievable from the graph.
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
||||||
|
firstClosingNonce, err := c.cfg.MusigSession.ClosingNonce()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown.ShutdownNonce = (*lnwire.ShutdownNonce)(
|
||||||
|
&firstClosingNonce.PubNonce,
|
||||||
|
)
|
||||||
|
|
||||||
|
chancloserLog.Infof("Initiating shutdown w/ nonce: %v",
|
||||||
|
spew.Sdump(firstClosingNonce.PubNonce))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before closing, we'll attempt to send a disable update for the
|
||||||
|
// channel. We do so before closing the channel as otherwise the
|
||||||
|
// current edge policy won't be retrievable from the graph.
|
||||||
if err := c.cfg.DisableChannel(c.chanPoint); err != nil {
|
if err := c.cfg.DisableChannel(c.chanPoint); err != nil {
|
||||||
chancloserLog.Warnf("Unable to disable channel %v on close: %v",
|
chancloserLog.Warnf("Unable to disable channel %v on close: %v",
|
||||||
c.chanPoint, err)
|
c.chanPoint, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before continuing, mark the channel as cooperatively closed with a nil
|
// Before continuing, mark the channel as cooperatively closed with a
|
||||||
// txn. Even though we haven't negotiated the final txn, this guarantees
|
// nil txn. Even though we haven't negotiated the final txn, this
|
||||||
// that our listchannels rpc will be externally consistent, and reflect
|
// guarantees that our listchannels rpc will be externally consistent,
|
||||||
// that the channel is being shutdown by the time the closing request
|
// and reflect that the channel is being shutdown by the time the
|
||||||
// returns.
|
// closing request returns.
|
||||||
err := c.cfg.Channel.MarkCoopBroadcasted(nil, c.locallyInitiated)
|
err := c.cfg.Channel.MarkCoopBroadcasted(nil, c.locallyInitiated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -448,6 +405,7 @@ func (c *ChanCloser) CloseRequest() *htlcswitch.ChanClose {
|
|||||||
// NOTE: This method will PANIC if the underlying channel implementation isn't
|
// NOTE: This method will PANIC if the underlying channel implementation isn't
|
||||||
// the desired type.
|
// the desired type.
|
||||||
func (c *ChanCloser) Channel() *lnwallet.LightningChannel {
|
func (c *ChanCloser) Channel() *lnwallet.LightningChannel {
|
||||||
|
// TODO(roasbeef): remove this
|
||||||
return c.cfg.Channel.(*lnwallet.LightningChannel)
|
return c.cfg.Channel.(*lnwallet.LightningChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,8 +480,9 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
// as otherwise, this is an attempted invalid state transition.
|
// as otherwise, this is an attempted invalid state transition.
|
||||||
shutdownMsg, ok := msg.(*lnwire.Shutdown)
|
shutdownMsg, ok := msg.(*lnwire.Shutdown)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false, fmt.Errorf("expected lnwire.Shutdown, instead "+
|
return nil, false, fmt.Errorf("expected "+
|
||||||
"have %v", spew.Sdump(msg))
|
"lnwire.Shutdown, instead have %v",
|
||||||
|
spew.Sdump(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// As we're the responder to this shutdown (the other party
|
// As we're the responder to this shutdown (the other party
|
||||||
@@ -571,6 +530,20 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a taproot channel, then we'll want to stash the
|
||||||
|
// remote nonces so we can properly create a new musig
|
||||||
|
// session for signing.
|
||||||
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
||||||
|
if shutdownMsg.ShutdownNonce == nil {
|
||||||
|
return nil, false, fmt.Errorf("shutdown " +
|
||||||
|
"nonce not populated")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
|
||||||
|
PubNonce: *shutdownMsg.ShutdownNonce,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
chancloserLog.Infof("ChannelPoint(%v): responding to shutdown",
|
chancloserLog.Infof("ChannelPoint(%v): responding to shutdown",
|
||||||
c.chanPoint)
|
c.chanPoint)
|
||||||
|
|
||||||
@@ -588,7 +561,8 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
if chanInitiator {
|
if chanInitiator {
|
||||||
closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
|
closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, fmt.Errorf("unable to sign "+
|
||||||
|
"new co op close offer: %w", err)
|
||||||
}
|
}
|
||||||
msgsToSend = append(msgsToSend, closeSigned)
|
msgsToSend = append(msgsToSend, closeSigned)
|
||||||
}
|
}
|
||||||
@@ -628,10 +602,24 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
// closing transaction should look like.
|
// closing transaction should look like.
|
||||||
c.state = closeFeeNegotiation
|
c.state = closeFeeNegotiation
|
||||||
|
|
||||||
// Now that we know their desried delivery script, we can
|
// Now that we know their desired delivery script, we can
|
||||||
// compute what our max/ideal fee will be.
|
// compute what our max/ideal fee will be.
|
||||||
c.initFeeBaseline()
|
c.initFeeBaseline()
|
||||||
|
|
||||||
|
// If this is a taproot channel, then we'll want to stash the
|
||||||
|
// local+remote nonces so we can properly create a new musig
|
||||||
|
// session for signing.
|
||||||
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
||||||
|
if shutdownMsg.ShutdownNonce == nil {
|
||||||
|
return nil, false, fmt.Errorf("shutdown " +
|
||||||
|
"nonce not populated")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
|
||||||
|
PubNonce: *shutdownMsg.ShutdownNonce,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
chancloserLog.Infof("ChannelPoint(%v): shutdown response received, "+
|
chancloserLog.Infof("ChannelPoint(%v): shutdown response received, "+
|
||||||
"entering fee negotiation", c.chanPoint)
|
"entering fee negotiation", c.chanPoint)
|
||||||
|
|
||||||
@@ -641,7 +629,8 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
if c.cfg.Channel.IsInitiator() {
|
if c.cfg.Channel.IsInitiator() {
|
||||||
closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
|
closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, fmt.Errorf("unable to sign "+
|
||||||
|
"new co op close offer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []lnwire.Message{closeSigned}, false, nil
|
return []lnwire.Message{closeSigned}, false, nil
|
||||||
@@ -661,14 +650,68 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
"instead have %v", spew.Sdump(msg))
|
"instead have %v", spew.Sdump(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll compare the proposed total fee, to what we've proposed during
|
// If this is a taproot channel, then it MUST have a partial
|
||||||
// the negotiations. If it doesn't match any of our prior offers, then
|
// signature set at this point.
|
||||||
// we'll attempt to ratchet the fee closer to
|
isTaproot := c.cfg.Channel.ChanType().IsTaproot()
|
||||||
|
if isTaproot && closeSignedMsg.PartialSig == nil {
|
||||||
|
return nil, false, fmt.Errorf("partial sig not set " +
|
||||||
|
"for taproot chan")
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitiator := c.cfg.Channel.IsInitiator()
|
||||||
|
|
||||||
|
// We'll compare the proposed total fee, to what we've proposed
|
||||||
|
// during the negotiations. If it doesn't match any of our
|
||||||
|
// prior offers, then we'll attempt to ratchet the fee closer
|
||||||
|
// to our ideal fee.
|
||||||
remoteProposedFee := closeSignedMsg.FeeSatoshis
|
remoteProposedFee := closeSignedMsg.FeeSatoshis
|
||||||
if _, ok := c.priorFeeOffers[remoteProposedFee]; !ok {
|
|
||||||
// We'll now attempt to ratchet towards a fee deemed acceptable by
|
_, feeMatchesOffer := c.priorFeeOffers[remoteProposedFee]
|
||||||
// both parties, factoring in our ideal fee rate, and the last
|
switch {
|
||||||
// proposed fee by both sides.
|
// For taproot channels, since nonces are involved, we can't do
|
||||||
|
// the existing co-op close negotiation process without going
|
||||||
|
// to a fully round based model. Rather than do this, we'll
|
||||||
|
// just accept the very first offer by the initiator.
|
||||||
|
case isTaproot && !isInitiator:
|
||||||
|
chancloserLog.Infof("ChannelPoint(%v) accepting "+
|
||||||
|
"initiator fee of %v", c.chanPoint,
|
||||||
|
remoteProposedFee)
|
||||||
|
|
||||||
|
// To auto-accept the initiators proposal, we'll just
|
||||||
|
// send back a signature w/ the same offer. We don't
|
||||||
|
// send the message here, as we can drop down and
|
||||||
|
// finalize the closure and broadcast, then echo back
|
||||||
|
// to Alice the final signature.
|
||||||
|
_, err := c.proposeCloseSigned(remoteProposedFee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("unable to sign "+
|
||||||
|
"new co op close offer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
// Otherwise, if we are the initiator, and we just sent a
|
||||||
|
// signature for a taproot channel, then we'll ensure that the
|
||||||
|
// fee rate matches up exactly.
|
||||||
|
case isTaproot && isInitiator && !feeMatchesOffer:
|
||||||
|
return nil, false, fmt.Errorf("fee rate for "+
|
||||||
|
"taproot channels was not accepted: "+
|
||||||
|
"sent %v, got %v",
|
||||||
|
c.idealFeeSat, remoteProposedFee)
|
||||||
|
|
||||||
|
// If we're the initiator of the taproot channel, and we had
|
||||||
|
// our fee echo'd back, then it's all good, and we can proceed
|
||||||
|
// with final broadcast.
|
||||||
|
case isTaproot && isInitiator && feeMatchesOffer:
|
||||||
|
break
|
||||||
|
|
||||||
|
// Otherwise, if this is a normal segwit v0 channel, and the
|
||||||
|
// fee doesn't match our offer, then we'll try to "negotiate" a
|
||||||
|
// new fee.
|
||||||
|
case !feeMatchesOffer:
|
||||||
|
// We'll now attempt to ratchet towards a fee deemed
|
||||||
|
// acceptable by both parties, factoring in our ideal
|
||||||
|
// fee rate, and the last proposed fee by both sides.
|
||||||
feeProposal := calcCompromiseFee(c.chanPoint, c.idealFeeSat,
|
feeProposal := calcCompromiseFee(c.chanPoint, c.idealFeeSat,
|
||||||
c.lastFeeProposal, remoteProposedFee,
|
c.lastFeeProposal, remoteProposedFee,
|
||||||
)
|
)
|
||||||
@@ -678,17 +721,19 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
c.maxFee)
|
c.maxFee)
|
||||||
}
|
}
|
||||||
|
|
||||||
// With our new fee proposal calculated, we'll craft a new close
|
// With our new fee proposal calculated, we'll craft a
|
||||||
// signed signature to send to the other party so we can continue
|
// new close signed signature to send to the other
|
||||||
// the fee negotiation process.
|
// party so we can continue the fee negotiation
|
||||||
|
// process.
|
||||||
closeSigned, err := c.proposeCloseSigned(feeProposal)
|
closeSigned, err := c.proposeCloseSigned(feeProposal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, fmt.Errorf("unable to sign "+
|
||||||
|
"new co op close offer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the compromise fee doesn't match what the peer proposed, then
|
// If the compromise fee doesn't match what the peer
|
||||||
// we'll return this latest close signed message so we can continue
|
// proposed, then we'll return this latest close signed
|
||||||
// negotiation.
|
// message so we can continue negotiation.
|
||||||
if feeProposal != remoteProposedFee {
|
if feeProposal != remoteProposedFee {
|
||||||
chancloserLog.Debugf("ChannelPoint(%v): close tx fee "+
|
chancloserLog.Debugf("ChannelPoint(%v): close tx fee "+
|
||||||
"disagreement, continuing negotiation", c.chanPoint)
|
"disagreement, continuing negotiation", c.chanPoint)
|
||||||
@@ -699,38 +744,56 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, ending "+
|
chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, ending "+
|
||||||
"negotiation", c.chanPoint, remoteProposedFee)
|
"negotiation", c.chanPoint, remoteProposedFee)
|
||||||
|
|
||||||
// Otherwise, we've agreed on a fee for the closing transaction! We'll
|
// Otherwise, we've agreed on a fee for the closing
|
||||||
// craft the final closing transaction so we can broadcast it to the
|
// transaction! We'll craft the final closing transaction so we
|
||||||
// network.
|
// can broadcast it to the network.
|
||||||
matchingSig := c.priorFeeOffers[remoteProposedFee].Signature
|
var (
|
||||||
localSig, err := matchingSig.ToSignature()
|
localSig, remoteSig input.Signature
|
||||||
if err != nil {
|
closeOpts []lnwallet.ChanCloseOpt
|
||||||
return nil, false, err
|
err error
|
||||||
}
|
)
|
||||||
|
matchingSig := c.priorFeeOffers[remoteProposedFee]
|
||||||
remoteSig, err := closeSignedMsg.Signature.ToSignature()
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
||||||
if err != nil {
|
muSession := c.cfg.MusigSession
|
||||||
return nil, false, err
|
localSig, remoteSig, closeOpts, err = muSession.CombineClosingOpts( //nolint:ll
|
||||||
|
*matchingSig.PartialSig,
|
||||||
|
*closeSignedMsg.PartialSig,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localSig, err = matchingSig.Signature.ToSignature()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
remoteSig, err = closeSignedMsg.Signature.ToSignature()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose(
|
closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose(
|
||||||
localSig, remoteSig, c.localDeliveryScript, c.remoteDeliveryScript,
|
localSig, remoteSig, c.localDeliveryScript,
|
||||||
remoteProposedFee,
|
c.remoteDeliveryScript, remoteProposedFee, closeOpts...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
c.closingTx = closeTx
|
c.closingTx = closeTx
|
||||||
|
|
||||||
// Before publishing the closing tx, we persist it to the database,
|
// Before publishing the closing tx, we persist it to the
|
||||||
// such that it can be republished if something goes wrong.
|
// database, such that it can be republished if something goes
|
||||||
err = c.cfg.Channel.MarkCoopBroadcasted(closeTx, c.locallyInitiated)
|
// wrong.
|
||||||
|
err = c.cfg.Channel.MarkCoopBroadcasted(
|
||||||
|
closeTx, c.locallyInitiated,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// With the closing transaction crafted, we'll now broadcast it to the
|
// With the closing transaction crafted, we'll now broadcast it
|
||||||
// network.
|
// to the network.
|
||||||
chancloserLog.Infof("Broadcasting cooperative close tx: %v",
|
chancloserLog.Infof("Broadcasting cooperative close tx: %v",
|
||||||
newLogClosure(func() string {
|
newLogClosure(func() string {
|
||||||
return spew.Sdump(closeTx)
|
return spew.Sdump(closeTx)
|
||||||
@@ -747,10 +810,11 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, we'll transition to the closeFinished state, and also
|
// Finally, we'll transition to the closeFinished state, and
|
||||||
// return the final close signed message we sent. Additionally, we
|
// also return the final close signed message we sent.
|
||||||
// return true for the second argument to indicate we're finished with
|
// Additionally, we return true for the second argument to
|
||||||
// the channel closing negotiation.
|
// indicate we're finished with the channel closing
|
||||||
|
// negotiation.
|
||||||
c.state = closeFinished
|
c.state = closeFinished
|
||||||
matchingOffer := c.priorFeeOffers[remoteProposedFee]
|
matchingOffer := c.priorFeeOffers[remoteProposedFee]
|
||||||
return []lnwire.Message{matchingOffer}, true, nil
|
return []lnwire.Message{matchingOffer}, true, nil
|
||||||
@@ -778,8 +842,23 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
|
|||||||
// transaction for a channel based on the prior fee negotiations and our current
|
// transaction for a channel based on the prior fee negotiations and our current
|
||||||
// compromise fee.
|
// compromise fee.
|
||||||
func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSigned, error) {
|
func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSigned, error) {
|
||||||
|
var (
|
||||||
|
closeOpts []lnwallet.ChanCloseOpt
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// If this is a taproot channel, then we'll include the musig session
|
||||||
|
// generated for the next co-op close negotiation round.
|
||||||
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
||||||
|
closeOpts, err = c.cfg.MusigSession.ProposalClosingOpts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rawSig, _, _, err := c.cfg.Channel.CreateCloseProposal(
|
rawSig, _, _, err := c.cfg.Channel.CreateCloseProposal(
|
||||||
fee, c.localDeliveryScript, c.remoteDeliveryScript,
|
fee, c.localDeliveryScript, c.remoteDeliveryScript,
|
||||||
|
closeOpts...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -788,21 +867,42 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSign
|
|||||||
// We'll note our last signature and proposed fee so when the remote
|
// We'll note our last signature and proposed fee so when the remote
|
||||||
// party responds we'll be able to decide if we've agreed on fees or
|
// party responds we'll be able to decide if we've agreed on fees or
|
||||||
// not.
|
// not.
|
||||||
c.lastFeeProposal = fee
|
var (
|
||||||
|
parsedSig lnwire.Sig
|
||||||
|
partialSig *lnwire.PartialSigWithNonce
|
||||||
|
)
|
||||||
|
if c.cfg.Channel.ChanType().IsTaproot() {
|
||||||
|
musig, ok := rawSig.(*lnwallet.MusigPartialSig)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected MusigPartialSig, "+
|
||||||
|
"got %T", rawSig)
|
||||||
|
}
|
||||||
|
|
||||||
parsedSig, err := lnwire.NewSigFromSignature(rawSig)
|
partialSig = musig.ToWireSig()
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
parsedSig, err = lnwire.NewSigFromSignature(rawSig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.lastFeeProposal = fee
|
||||||
|
|
||||||
chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to "+
|
chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to "+
|
||||||
"close chan", c.chanPoint, int64(fee))
|
"close chan", c.chanPoint, int64(fee))
|
||||||
|
|
||||||
// We'll assemble a ClosingSigned message using this information and return
|
// We'll assemble a ClosingSigned message using this information and
|
||||||
// it to the caller so we can kick off the final stage of the channel
|
// return it to the caller so we can kick off the final stage of the
|
||||||
// closure process.
|
// channel closure process.
|
||||||
closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig)
|
closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig)
|
||||||
|
|
||||||
|
// For musig2 channels, the main sig is blank, and instead we'll send
|
||||||
|
// over a partial signature which'll be combine donce our offer is
|
||||||
|
// accepted.
|
||||||
|
if partialSig != nil {
|
||||||
|
closeSignedMsg.PartialSig = &partialSig.PartialSig
|
||||||
|
}
|
||||||
|
|
||||||
// We'll also save this close signed, in the case that the remote party
|
// We'll also save this close signed, in the case that the remote party
|
||||||
// accepts our offer. This way, we don't have to re-sign.
|
// accepts our offer. This way, we don't have to re-sign.
|
||||||
c.priorFeeOffers[fee] = closeSignedMsg
|
c.priorFeeOffers[fee] = closeSignedMsg
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
@@ -12,6 +15,9 @@ import (
|
|||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/lightningnetwork/lnd/lnutils"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -135,6 +141,9 @@ type mockChannel struct {
|
|||||||
chanPoint wire.OutPoint
|
chanPoint wire.OutPoint
|
||||||
initiator bool
|
initiator bool
|
||||||
scid lnwire.ShortChannelID
|
scid lnwire.ShortChannelID
|
||||||
|
chanType channeldb.ChannelType
|
||||||
|
localKey keychain.KeyDescriptor
|
||||||
|
remoteKey keychain.KeyDescriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockChannel) ChannelPoint() *wire.OutPoint {
|
func (m *mockChannel) ChannelPoint() *wire.OutPoint {
|
||||||
@@ -162,15 +171,26 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
|
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
|
||||||
localScript, remoteScript []byte,
|
localScript, remoteScript []byte, _ ...lnwallet.ChanCloseOpt,
|
||||||
) (input.Signature, *chainhash.Hash, btcutil.Amount, error) {
|
) (input.Signature, *chainhash.Hash, btcutil.Amount, error) {
|
||||||
|
|
||||||
|
if m.chanType.IsTaproot() {
|
||||||
|
return lnwallet.NewMusigPartialSig(
|
||||||
|
&musig2.PartialSignature{
|
||||||
|
S: new(btcec.ModNScalar),
|
||||||
|
R: new(btcec.PublicKey),
|
||||||
|
},
|
||||||
|
lnwire.Musig2Nonce{}, lnwire.Musig2Nonce{}, nil,
|
||||||
|
), nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil, nil, 0, nil
|
return nil, nil, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockChannel) CompleteCooperativeClose(localSig,
|
func (m *mockChannel) CompleteCooperativeClose(localSig,
|
||||||
remoteSig input.Signature, localScript, remoteScript []byte,
|
remoteSig input.Signature, localScript, remoteScript []byte,
|
||||||
proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) {
|
proposedFee btcutil.Amount,
|
||||||
|
_ ...lnwallet.ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) {
|
||||||
|
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
@@ -183,6 +203,73 @@ func (m *mockChannel) RemoteBalanceDust() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockChannel) ChanType() channeldb.ChannelType {
|
||||||
|
return m.chanType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockChannel) FundingTxOut() *wire.TxOut {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockChannel) MultiSigKeys() (keychain.KeyDescriptor, keychain.KeyDescriptor) {
|
||||||
|
return m.localKey, m.remoteKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockTaprootChan(t *testing.T, initiator bool) *mockChannel {
|
||||||
|
taprootBits := channeldb.SimpleTaprootFeatureBit |
|
||||||
|
channeldb.AnchorOutputsBit |
|
||||||
|
channeldb.ZeroHtlcTxFeeBit |
|
||||||
|
channeldb.SingleFunderTweaklessBit
|
||||||
|
|
||||||
|
localKey, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
remoteKey, err := btcec.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &mockChannel{
|
||||||
|
chanPoint: wire.OutPoint{
|
||||||
|
Hash: chainhash.Hash{},
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
initiator: initiator,
|
||||||
|
chanType: taprootBits,
|
||||||
|
localKey: keychain.KeyDescriptor{
|
||||||
|
PubKey: localKey.PubKey(),
|
||||||
|
},
|
||||||
|
remoteKey: keychain.KeyDescriptor{
|
||||||
|
PubKey: remoteKey.PubKey(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockMusigSession struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockMusigSession() *mockMusigSession {
|
||||||
|
return &mockMusigSession{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMusigSession) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMusigSession) CombineClosingOpts(localSig,
|
||||||
|
remoteSig lnwire.PartialSig,
|
||||||
|
) (input.Signature, input.Signature, []lnwallet.ChanCloseOpt, error) {
|
||||||
|
|
||||||
|
return &lnwallet.MusigPartialSig{}, &lnwallet.MusigPartialSig{}, nil,
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) {
|
||||||
|
return &musig2.Nonces{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMusigSession) InitRemoteNonce(nonce *musig2.Nonces) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type mockCoopFeeEstimator struct {
|
type mockCoopFeeEstimator struct {
|
||||||
targetFee btcutil.Amount
|
targetFee btcutil.Amount
|
||||||
}
|
}
|
||||||
@@ -377,3 +464,130 @@ func TestParseUpfrontShutdownAddress(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertType[T any](t *testing.T, typ any) T {
|
||||||
|
value, ok := typ.(T)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTaprootFastClose tests that we are able to properly execute a fast close
|
||||||
|
// (skip negotiation) for taproot channels.
|
||||||
|
func TestTaprootFastClose(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
aliceChan := newMockTaprootChan(t, true)
|
||||||
|
bobChan := newMockTaprootChan(t, false)
|
||||||
|
|
||||||
|
broadcastSignal := make(chan struct{}, 2)
|
||||||
|
|
||||||
|
idealFee := chainfee.SatPerKWeight(506)
|
||||||
|
|
||||||
|
// Next, we'll make a channel for Alice and Bob, with Alice being the
|
||||||
|
// initiator.
|
||||||
|
aliceCloser := NewChanCloser(
|
||||||
|
ChanCloseCfg{
|
||||||
|
Channel: aliceChan,
|
||||||
|
MusigSession: newMockMusigSession(),
|
||||||
|
BroadcastTx: func(_ *wire.MsgTx, _ string) error {
|
||||||
|
broadcastSignal <- struct{}{}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MaxFee: chainfee.SatPerKWeight(1000),
|
||||||
|
FeeEstimator: &SimpleCoopFeeEstimator{},
|
||||||
|
DisableChannel: func(wire.OutPoint) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, nil, idealFee, 0, nil, true,
|
||||||
|
)
|
||||||
|
aliceCloser.initFeeBaseline()
|
||||||
|
|
||||||
|
bobCloser := NewChanCloser(
|
||||||
|
ChanCloseCfg{
|
||||||
|
Channel: bobChan,
|
||||||
|
MusigSession: newMockMusigSession(),
|
||||||
|
MaxFee: chainfee.SatPerKWeight(1000),
|
||||||
|
BroadcastTx: func(_ *wire.MsgTx, _ string) error {
|
||||||
|
broadcastSignal <- struct{}{}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
FeeEstimator: &SimpleCoopFeeEstimator{},
|
||||||
|
DisableChannel: func(wire.OutPoint) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, nil, idealFee, 0, nil, false,
|
||||||
|
)
|
||||||
|
bobCloser.initFeeBaseline()
|
||||||
|
|
||||||
|
// With our set up complete, we'll now initialize the shutdown
|
||||||
|
// procedure kicked off by Alice.
|
||||||
|
msg, err := aliceCloser.ShutdownChan()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, msg)
|
||||||
|
|
||||||
|
// Bob will then process this message. As he's the responder, he should
|
||||||
|
// only send the shutdown message back to Alice.
|
||||||
|
bobMsgs, closeFinished, err := bobCloser.ProcessCloseMsg(msg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, closeFinished)
|
||||||
|
require.Len(t, bobMsgs, 1)
|
||||||
|
require.IsType(t, &lnwire.Shutdown{}, bobMsgs[0])
|
||||||
|
|
||||||
|
// Alice should process the shutdown message, and create a closing
|
||||||
|
// signed of her own.
|
||||||
|
aliceMsgs, closeFinished, err := aliceCloser.ProcessCloseMsg(bobMsgs[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, closeFinished)
|
||||||
|
require.Len(t, aliceMsgs, 1)
|
||||||
|
require.IsType(t, &lnwire.ClosingSigned{}, aliceMsgs[0])
|
||||||
|
|
||||||
|
// Next, Bob will process the closing signed message, and send back a
|
||||||
|
// new one that should match exactly the offer Alice sent.
|
||||||
|
bobMsgs, closeFinished, err = bobCloser.ProcessCloseMsg(aliceMsgs[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, closeFinished)
|
||||||
|
require.Len(t, aliceMsgs, 1)
|
||||||
|
require.IsType(t, &lnwire.ClosingSigned{}, bobMsgs[0])
|
||||||
|
|
||||||
|
// At this point, Bob has accepted the offer, so he can broadcast the
|
||||||
|
// closing transaction, and considers the channel closed.
|
||||||
|
_, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Bob's fee proposal should exactly match Alice's initial fee.
|
||||||
|
aliceOffer := assertType[*lnwire.ClosingSigned](t, aliceMsgs[0])
|
||||||
|
bobOffer := assertType[*lnwire.ClosingSigned](t, bobMsgs[0])
|
||||||
|
require.Equal(t, aliceOffer.FeeSatoshis, bobOffer.FeeSatoshis)
|
||||||
|
|
||||||
|
// If we modify Bob's offer, and try to have Alice process it, then she
|
||||||
|
// should reject it.
|
||||||
|
ogOffer := bobOffer.FeeSatoshis
|
||||||
|
bobOffer.FeeSatoshis /= 2
|
||||||
|
|
||||||
|
aliceMsgs, _, err = aliceCloser.ProcessCloseMsg(bobOffer)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "was not accepted")
|
||||||
|
|
||||||
|
// We'll now restore the original offer before passing it on to Alice.
|
||||||
|
bobOffer.FeeSatoshis = ogOffer
|
||||||
|
|
||||||
|
// If we use the original offer, then Alice should accept this message,
|
||||||
|
// and finalize the shutdown process. We expect a message here as Alice
|
||||||
|
// will echo back the final message.
|
||||||
|
aliceMsgs, closeFinished, err = aliceCloser.ProcessCloseMsg(bobMsgs[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, closeFinished)
|
||||||
|
require.Len(t, aliceMsgs, 1)
|
||||||
|
require.IsType(t, &lnwire.ClosingSigned{}, aliceMsgs[0])
|
||||||
|
|
||||||
|
// Alice should now also broadcast her closing transaction.
|
||||||
|
_, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Finally, Bob will process Alice's echo message, and conclude.
|
||||||
|
bobMsgs, closeFinished, err = bobCloser.ProcessCloseMsg(aliceMsgs[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, closeFinished)
|
||||||
|
require.Len(t, bobMsgs, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package chancloser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoopFeeEstimator is used to estimate the fee of a co-op close transaction.
|
||||||
|
type CoopFeeEstimator interface {
|
||||||
|
// EstimateFee estimates an _absolute_ fee for a co-op close transaction
|
||||||
|
// given the local+remote tx outs (for the co-op close transaction),
|
||||||
|
// channel type, and ideal fee rate. If a passed TxOut is nil, then
|
||||||
|
// that indicates that an output is dust on the co-op close transaction
|
||||||
|
// _before_ fees are accounted for.
|
||||||
|
EstimateFee(chanType channeldb.ChannelType,
|
||||||
|
localTxOut, remoteTxOut *wire.TxOut,
|
||||||
|
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel abstracts away from the core channel state machine by exposing an
|
||||||
|
// interface that requires only the methods we need to carry out the channel
|
||||||
|
// closing process.
|
||||||
|
type Channel interface {
|
||||||
|
// ChannelPoint returns the channel point of the target channel.
|
||||||
|
ChannelPoint() *wire.OutPoint
|
||||||
|
|
||||||
|
// MarkCoopBroadcasted persistently marks that the channel close
|
||||||
|
// transaction has been broadcast.
|
||||||
|
MarkCoopBroadcasted(*wire.MsgTx, bool) error
|
||||||
|
|
||||||
|
// IsInitiator returns true we are the initiator of the channel.
|
||||||
|
IsInitiator() bool
|
||||||
|
|
||||||
|
// ShortChanID returns the scid of the channel.
|
||||||
|
ShortChanID() lnwire.ShortChannelID
|
||||||
|
|
||||||
|
// ChanType returns the channel type of the channel.
|
||||||
|
ChanType() channeldb.ChannelType
|
||||||
|
|
||||||
|
// FundingTxOut returns the funding output of the channel.
|
||||||
|
FundingTxOut() *wire.TxOut
|
||||||
|
|
||||||
|
// AbsoluteThawHeight returns the absolute thaw height of the channel.
|
||||||
|
// If the channel is pending, or an unconfirmed zero conf channel, then
|
||||||
|
// an error should be returned.
|
||||||
|
AbsoluteThawHeight() (uint32, error)
|
||||||
|
|
||||||
|
// LocalBalanceDust returns true if when creating a co-op close
|
||||||
|
// transaction, the balance of the local party will be dust after
|
||||||
|
// accounting for any anchor outputs.
|
||||||
|
LocalBalanceDust() bool
|
||||||
|
|
||||||
|
// RemoteBalanceDust returns true if when creating a co-op close
|
||||||
|
// transaction, the balance of the remote party will be dust after
|
||||||
|
// accounting for any anchor outputs.
|
||||||
|
RemoteBalanceDust() bool
|
||||||
|
|
||||||
|
// RemoteUpfrontShutdownScript returns the upfront shutdown script of
|
||||||
|
// the remote party. If the remote party didn't specify such a script,
|
||||||
|
// an empty delivery address should be returned.
|
||||||
|
RemoteUpfrontShutdownScript() lnwire.DeliveryAddress
|
||||||
|
|
||||||
|
// CreateCloseProposal creates a new co-op close proposal in the form
|
||||||
|
// of a valid signature, the chainhash of the final txid, and our final
|
||||||
|
// balance in the created state.
|
||||||
|
CreateCloseProposal(proposedFee btcutil.Amount, localDeliveryScript []byte,
|
||||||
|
remoteDeliveryScript []byte,
|
||||||
|
closeOpt ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
||||||
|
btcutil.Amount, error)
|
||||||
|
|
||||||
|
// CompleteCooperativeClose persistently "completes" the cooperative
|
||||||
|
// close by producing a fully signed co-op close transaction.
|
||||||
|
CompleteCooperativeClose(localSig, remoteSig input.Signature,
|
||||||
|
localDeliveryScript, remoteDeliveryScript []byte,
|
||||||
|
proposedFee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt,
|
||||||
|
) (*wire.MsgTx, btcutil.Amount, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MusigSession is an interface that abstracts away the details of the musig2
|
||||||
|
// session details. A session is used to generate the necessary closing options
|
||||||
|
// needed to close a channel cooperatively.
|
||||||
|
type MusigSession interface {
|
||||||
|
// ProposalClosingOpts generates the set of closing options needed to
|
||||||
|
// generate a new musig2 proposal signature.
|
||||||
|
ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, error)
|
||||||
|
|
||||||
|
// CombineClosingOpts returns the options that should be used when
|
||||||
|
// combining the final musig partial signature. The method also maps
|
||||||
|
// the lnwire partial signatures into an input.Signature that can be
|
||||||
|
// used more generally.
|
||||||
|
CombineClosingOpts(localSig, remoteSig lnwire.PartialSig,
|
||||||
|
) (input.Signature, input.Signature, []lnwallet.ChanCloseOpt, error)
|
||||||
|
|
||||||
|
// ClosingNonce generates the nonce we'll use to generate the musig2
|
||||||
|
// partial signatures for the co-op close transaction.
|
||||||
|
ClosingNonce() (*musig2.Nonces, error)
|
||||||
|
|
||||||
|
// InitRemoteNonce saves the remote nonce the party sent during their
|
||||||
|
// shutdown message so it can be used later to generate and verify
|
||||||
|
// signatures.
|
||||||
|
InitRemoteNonce(nonce *musig2.Nonces)
|
||||||
|
}
|
||||||
+142
-27
@@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/shachain"
|
"github.com/lightningnetwork/lnd/shachain"
|
||||||
@@ -7091,6 +7092,30 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chanCloseOpt is a functional option that can be used to modify the co-op
|
||||||
|
// close process.
|
||||||
|
type chanCloseOpt struct {
|
||||||
|
musigSession *MusigSession
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChanCloseOpt is a closure type that cen be used to modify the set of default
|
||||||
|
// options.
|
||||||
|
type ChanCloseOpt func(*chanCloseOpt)
|
||||||
|
|
||||||
|
// defaultCloseOpts is the default set of close options.
|
||||||
|
func defaultCloseOpts() *chanCloseOpt {
|
||||||
|
return &chanCloseOpt{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCoopCloseMusigSession can be used to apply an existing musig2 session to
|
||||||
|
// the cooperative close process. If specified, then a musig2 co-op close
|
||||||
|
// (single sig keyspend) will be used.
|
||||||
|
func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt {
|
||||||
|
return func(opts *chanCloseOpt) {
|
||||||
|
opts.musigSession = session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCloseProposal is used by both parties in a cooperative channel close
|
// CreateCloseProposal is used by both parties in a cooperative channel close
|
||||||
// workflow to generate proposed close transactions and signatures. This method
|
// workflow to generate proposed close transactions and signatures. This method
|
||||||
// should only be executed once all pending HTLCs (if any) on the channel have
|
// should only be executed once all pending HTLCs (if any) on the channel have
|
||||||
@@ -7102,8 +7127,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
|
|||||||
// TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs,
|
// TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs,
|
||||||
// settle any in flight.
|
// settle any in flight.
|
||||||
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||||
localDeliveryScript []byte,
|
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||||
remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash,
|
closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
||||||
btcutil.Amount, error) {
|
btcutil.Amount, error) {
|
||||||
|
|
||||||
lc.Lock()
|
lc.Lock()
|
||||||
@@ -7115,6 +7140,11 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||||||
return nil, nil, 0, ErrChanClosing
|
return nil, nil, 0, ErrChanClosing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opts := defaultCloseOpts()
|
||||||
|
for _, optFunc := range closeOpts {
|
||||||
|
optFunc(opts)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the final balances after subtracting the proposed fee, taking
|
// Get the final balances after subtracting the proposed fee, taking
|
||||||
// care not to persist the adjusted balance, as the feeRate may change
|
// care not to persist the adjusted balance, as the feeRate may change
|
||||||
// during the channel closing process.
|
// during the channel closing process.
|
||||||
@@ -7140,14 +7170,25 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||||||
return nil, nil, 0, err
|
return nil, nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, sign the completed cooperative closure transaction. As the
|
// If we have a co-op close musig session, then this is a taproot
|
||||||
// initiator we'll simply send our signature over to the remote party,
|
// channel, so we'll generate a _partial_ signature.
|
||||||
// using the generated txid to be notified once the closure transaction
|
var sig input.Signature
|
||||||
// has been confirmed.
|
if opts.musigSession != nil {
|
||||||
lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx)
|
sig, err = opts.musigSession.SignCommit(closeTx)
|
||||||
sig, err := lc.Signer.SignOutputRaw(closeTx, lc.signDesc)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, nil, 0, err
|
||||||
return nil, nil, 0, err
|
}
|
||||||
|
} else {
|
||||||
|
// For regular channels we'll, sign the completed cooperative
|
||||||
|
// closure transaction. As the initiator we'll simply send our
|
||||||
|
// signature over to the remote party, using the generated txid
|
||||||
|
// to be notified once the closure transaction has been
|
||||||
|
// confirmed.
|
||||||
|
lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx)
|
||||||
|
sig, err = lc.Signer.SignOutputRaw(closeTx, lc.signDesc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// As everything checks out, indicate in the channel status that a
|
// As everything checks out, indicate in the channel status that a
|
||||||
@@ -7168,7 +7209,8 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||||||
func (lc *LightningChannel) CompleteCooperativeClose(
|
func (lc *LightningChannel) CompleteCooperativeClose(
|
||||||
localSig, remoteSig input.Signature,
|
localSig, remoteSig input.Signature,
|
||||||
localDeliveryScript, remoteDeliveryScript []byte,
|
localDeliveryScript, remoteDeliveryScript []byte,
|
||||||
proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) {
|
proposedFee btcutil.Amount,
|
||||||
|
closeOpts ...ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) {
|
||||||
|
|
||||||
lc.Lock()
|
lc.Lock()
|
||||||
defer lc.Unlock()
|
defer lc.Unlock()
|
||||||
@@ -7179,6 +7221,11 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
|||||||
return nil, 0, ErrChanClosing
|
return nil, 0, ErrChanClosing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opts := defaultCloseOpts()
|
||||||
|
for _, optFunc := range closeOpts {
|
||||||
|
optFunc(opts)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the final balances after subtracting the proposed fee.
|
// Get the final balances after subtracting the proposed fee.
|
||||||
ourBalance, theirBalance, err := CoopCloseBalance(
|
ourBalance, theirBalance, err := CoopCloseBalance(
|
||||||
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
||||||
@@ -7201,31 +7248,62 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
|||||||
// consensus rules such as being too big, or having any value with a
|
// consensus rules such as being too big, or having any value with a
|
||||||
// negative output.
|
// negative output.
|
||||||
tx := btcutil.NewTx(closeTx)
|
tx := btcutil.NewTx(closeTx)
|
||||||
|
prevOut := lc.signDesc.Output
|
||||||
if err := blockchain.CheckTransactionSanity(tx); err != nil {
|
if err := blockchain.CheckTransactionSanity(tx); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
hashCache := input.NewTxSigHashesV0Only(closeTx)
|
|
||||||
|
|
||||||
// Finally, construct the witness stack minding the order of the
|
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
|
||||||
// pubkeys+sigs on the stack.
|
prevOut.PkScript, prevOut.Value,
|
||||||
ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
|
|
||||||
SerializeCompressed()
|
|
||||||
theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
|
|
||||||
SerializeCompressed()
|
|
||||||
witness := input.SpendMultiSig(
|
|
||||||
lc.signDesc.WitnessScript, ourKey, localSig, theirKey,
|
|
||||||
remoteSig,
|
|
||||||
)
|
)
|
||||||
closeTx.TxIn[0].Witness = witness
|
hashCache := txscript.NewTxSigHashes(closeTx, prevOutputFetcher)
|
||||||
|
|
||||||
|
// Next, we'll complete the co-op close transaction. Depending on the
|
||||||
|
// set of options, we'll either do a regular p2wsh spend, or construct
|
||||||
|
// the final schnorr signature from a set of partial sigs.
|
||||||
|
if opts.musigSession != nil {
|
||||||
|
// For taproot channels, we'll use the attached session to
|
||||||
|
// combine the two partial signatures into a proper schnorr
|
||||||
|
// signature.
|
||||||
|
remotePartialSig, ok := remoteSig.(*MusigPartialSig)
|
||||||
|
if !ok {
|
||||||
|
return nil, 0, fmt.Errorf("expected MusigPartialSig, "+
|
||||||
|
"got %T", remoteSig)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalSchnorrSig, err := opts.musigSession.CombineSigs(
|
||||||
|
remotePartialSig.sig,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("unable to combine "+
|
||||||
|
"final co-op close sig: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The witness for a keyspend is just the signature itself.
|
||||||
|
closeTx.TxIn[0].Witness = wire.TxWitness{
|
||||||
|
finalSchnorrSig.Serialize(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// For regular channels, we'll need to , construct the witness
|
||||||
|
// stack minding the order of the pubkeys+sigs on the stack.
|
||||||
|
ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
|
||||||
|
SerializeCompressed()
|
||||||
|
theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
|
||||||
|
SerializeCompressed()
|
||||||
|
witness := input.SpendMultiSig(
|
||||||
|
lc.signDesc.WitnessScript, ourKey, localSig, theirKey,
|
||||||
|
remoteSig,
|
||||||
|
)
|
||||||
|
closeTx.TxIn[0].Witness = witness
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the finalized transaction to ensure the output script is
|
// Validate the finalized transaction to ensure the output script is
|
||||||
// properly met, and that the remote peer supplied a valid signature.
|
// properly met, and that the remote peer supplied a valid signature.
|
||||||
prevOut := lc.signDesc.Output
|
|
||||||
vm, err := txscript.NewEngine(
|
vm, err := txscript.NewEngine(
|
||||||
prevOut.PkScript, closeTx, 0, txscript.StandardVerifyFlags, nil,
|
prevOut.PkScript, closeTx, 0, txscript.StandardVerifyFlags, nil,
|
||||||
hashCache, prevOut.Value, txscript.NewCannedPrevOutputFetcher(
|
hashCache, prevOut.Value, prevOutputFetcher,
|
||||||
prevOut.PkScript, prevOut.Value,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -7885,11 +7963,23 @@ func (lc *LightningChannel) IdealCommitFeeRate(netFeeRate, minRelayFeeRate,
|
|||||||
return absoluteMaxFee
|
return absoluteMaxFee
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoteNextRevocation returns the channelState's RemoteNextRevocation.
|
// RemoteNextRevocation returns the channelState's RemoteNextRevocation. For
|
||||||
|
// musig2 channels, until a nonce pair is processed by the remote party, a nil
|
||||||
|
// public key is returned.
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): revisit, maybe just make a more general method instead?
|
||||||
func (lc *LightningChannel) RemoteNextRevocation() *btcec.PublicKey {
|
func (lc *LightningChannel) RemoteNextRevocation() *btcec.PublicKey {
|
||||||
lc.RLock()
|
lc.RLock()
|
||||||
defer lc.RUnlock()
|
defer lc.RUnlock()
|
||||||
|
|
||||||
|
if !lc.channelState.ChanType.IsTaproot() {
|
||||||
|
return lc.channelState.RemoteNextRevocation
|
||||||
|
}
|
||||||
|
|
||||||
|
if lc.musigSessions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return lc.channelState.RemoteNextRevocation
|
return lc.channelState.RemoteNextRevocation
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8158,3 +8248,28 @@ func (lc *LightningChannel) InitRemoteMusigNonces(remoteNonce *musig2.Nonces,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChanType returns the channel type.
|
||||||
|
func (lc *LightningChannel) ChanType() channeldb.ChannelType {
|
||||||
|
lc.RLock()
|
||||||
|
defer lc.RUnlock()
|
||||||
|
|
||||||
|
return lc.channelState.ChanType
|
||||||
|
}
|
||||||
|
|
||||||
|
// FundingTxOut returns the funding output of the channel.
|
||||||
|
func (lc *LightningChannel) FundingTxOut() *wire.TxOut {
|
||||||
|
lc.RLock()
|
||||||
|
defer lc.RUnlock()
|
||||||
|
|
||||||
|
return &lc.fundingOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiSigKeys returns the set of multi-sig keys for an channel.
|
||||||
|
func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor, keychain.KeyDescriptor) {
|
||||||
|
lc.RLock()
|
||||||
|
defer lc.RUnlock()
|
||||||
|
|
||||||
|
return lc.channelState.LocalChanCfg.MultiSigKey,
|
||||||
|
lc.channelState.RemoteChanCfg.MultiSigKey
|
||||||
|
}
|
||||||
@@ -2903,6 +2903,7 @@ func (p *Brontide) createChanCloser(channel *lnwallet.LightningChannel,
|
|||||||
chanCloser := chancloser.NewChanCloser(
|
chanCloser := chancloser.NewChanCloser(
|
||||||
chancloser.ChanCloseCfg{
|
chancloser.ChanCloseCfg{
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
|
MusigSession: NewMusigChanCloser(channel),
|
||||||
FeeEstimator: &chancloser.SimpleCoopFeeEstimator{},
|
FeeEstimator: &chancloser.SimpleCoopFeeEstimator{},
|
||||||
BroadcastTx: p.cfg.Wallet.PublishTransaction,
|
BroadcastTx: p.cfg.Wallet.PublishTransaction,
|
||||||
DisableChannel: func(op wire.OutPoint) error {
|
DisableChannel: func(op wire.OutPoint) error {
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MusigChanCloser is an adapter over the normal channel state machine that
|
||||||
|
// allows the chan closer to handle the musig2 details of closing taproot
|
||||||
|
// channels.
|
||||||
|
type MusigChanCloser struct {
|
||||||
|
channel *lnwallet.LightningChannel
|
||||||
|
|
||||||
|
musigSession *lnwallet.MusigSession
|
||||||
|
|
||||||
|
localNonce *musig2.Nonces
|
||||||
|
remoteNonce *musig2.Nonces
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMusigChanCloser creates a new musig chan closer from a normal channel.
|
||||||
|
func NewMusigChanCloser(channel *lnwallet.LightningChannel) *MusigChanCloser {
|
||||||
|
return &MusigChanCloser{
|
||||||
|
channel: channel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProposalClosingOpts returns the options that should be used when
|
||||||
|
// generating a new co-op close signature.
|
||||||
|
func (m *MusigChanCloser) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, error) {
|
||||||
|
switch {
|
||||||
|
case m.localNonce == nil:
|
||||||
|
return nil, fmt.Errorf("local nonce not generated")
|
||||||
|
|
||||||
|
case m.remoteNonce == nil:
|
||||||
|
return nil, fmt.Errorf("remote nonce not generated")
|
||||||
|
}
|
||||||
|
|
||||||
|
localKey, remoteKey := m.channel.MultiSigKeys()
|
||||||
|
m.musigSession = lnwallet.NewPartialMusigSession(
|
||||||
|
*m.localNonce, localKey, remoteKey,
|
||||||
|
m.channel.Signer, m.channel.FundingTxOut(),
|
||||||
|
lnwallet.RemoteMusigCommit,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := m.musigSession.FinalizeSession(*m.remoteNonce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []lnwallet.ChanCloseOpt{
|
||||||
|
lnwallet.WithCoopCloseMusigSession(m.musigSession),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineClosingOpts returns the options that should be used when combining
|
||||||
|
// the final musig partial signature. The method also maps the lnwire partial
|
||||||
|
// signatures into an input.Signature that can be used more generally.
|
||||||
|
func (m *MusigChanCloser) CombineClosingOpts(localSig,
|
||||||
|
remoteSig lnwire.PartialSig) (input.Signature, input.Signature,
|
||||||
|
[]lnwallet.ChanCloseOpt, error) {
|
||||||
|
|
||||||
|
if m.musigSession == nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("musig session not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll convert the wire partial signatures into an input.Signature
|
||||||
|
// compliant struct so we can pass it into the final combination
|
||||||
|
// function.
|
||||||
|
localPartialSig := &lnwire.PartialSigWithNonce{
|
||||||
|
PartialSig: localSig,
|
||||||
|
Nonce: m.localNonce.PubNonce,
|
||||||
|
}
|
||||||
|
remotePartialSig := &lnwire.PartialSigWithNonce{
|
||||||
|
PartialSig: remoteSig,
|
||||||
|
Nonce: m.remoteNonce.PubNonce,
|
||||||
|
}
|
||||||
|
|
||||||
|
localMuSig := new(lnwallet.MusigPartialSig).FromWireSig(
|
||||||
|
localPartialSig,
|
||||||
|
)
|
||||||
|
remoteMuSig := new(lnwallet.MusigPartialSig).FromWireSig(
|
||||||
|
remotePartialSig,
|
||||||
|
)
|
||||||
|
|
||||||
|
opts := []lnwallet.ChanCloseOpt{
|
||||||
|
lnwallet.WithCoopCloseMusigSession(m.musigSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
// For taproot channels, we'll need to pass along the session so the
|
||||||
|
// final combined signature can be created.
|
||||||
|
return localMuSig, remoteMuSig, opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosingNonce returns the nonce that should be used when generating the our
|
||||||
|
// partial signature for the remote party.
|
||||||
|
func (m *MusigChanCloser) ClosingNonce() (*musig2.Nonces, error) {
|
||||||
|
if m.localNonce != nil {
|
||||||
|
return m.localNonce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
localKey, _ := m.channel.MultiSigKeys()
|
||||||
|
nonce, err := musig2.GenNonces(
|
||||||
|
musig2.WithPublicKey(localKey.PubKey),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.localNonce = nonce
|
||||||
|
|
||||||
|
return nonce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitRemoteNonce saves the remote nonce the party sent during their shutdown
|
||||||
|
// message so it can be used later to generate and verify signatures.
|
||||||
|
func (m *MusigChanCloser) InitRemoteNonce(nonce *musig2.Nonces) {
|
||||||
|
m.remoteNonce = nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compile-time assertion to ensure MusigChanCloser implements the
|
||||||
|
// chancloser.MusigSession interface.
|
||||||
|
var _ chancloser.MusigSession = (*MusigChanCloser)(nil)
|
||||||
Reference in new issue
Block a user