diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go new file mode 100644 index 0000000000..34cf98e284 --- /dev/null +++ b/lnwallet/musig_session.go @@ -0,0 +1,535 @@ +package lnwallet + +import ( + "bytes" + "fmt" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" +) + +// MusigPartialSig is a wrapper around the base musig2.PartialSignature type +// that also includes information about the set of nonces used, and also the +// signer. This allows us to implement the input.Signature interface, as that +// requires the ability to perform abstract verification based on a public key. +type MusigPartialSig struct { + // sig is the actual musig2 partial signature. + sig *musig2.PartialSignature + + // signerNonce is the nonce used by the signer to generate the partial + // signature. + signerNonce [musig2.PubNonceSize]byte + + // combinedNonce is the combined nonce of all signers. + combinedNonce [musig2.PubNonceSize]byte + + // signerKeys is the set of public keys of all signers. + signerKeys []*btcec.PublicKey +} + +// NewMusigPartialSig creates a new musig partial signature. +func NewMusigPartialSig(sig *musig2.PartialSignature, + signerNonce, combinedNonce [musig2.PubNonceSize]byte, + signerKeys []*btcec.PublicKey) *MusigPartialSig { + + return &MusigPartialSig{ + sig: sig, + signerNonce: signerNonce, + combinedNonce: combinedNonce, + signerKeys: signerKeys, + } +} + +// FromWireSig maps a wire partial sig to this internal type that we'll use to +// perform signature validation. +func (p *MusigPartialSig) FromWireSig(sig *lnwire.PartialSigWithNonce, +) *MusigPartialSig { + + p.sig = &musig2.PartialSignature{ + S: &sig.Sig, + } + p.signerNonce = sig.Nonce + + return p +} + +// ToWireSig maps the partial signature to something that we can use to write +// out for the wire protocol. +func (p *MusigPartialSig) ToWireSig() *lnwire.PartialSigWithNonce { + return &lnwire.PartialSigWithNonce{ + PartialSig: lnwire.NewPartialSig(*p.sig.S), + Nonce: p.signerNonce, + } +} + +// Serialize serializes the musig2 partial signature. The serializing includes +// the signer's public nonce _and_ the partial signature. The final signature +// is always 98 bytes in length. +func (p *MusigPartialSig) Serialize() []byte { + var b bytes.Buffer + + p.ToWireSig().Encode(&b) + + return b.Bytes() +} + +// ToSchnorrShell converts the musig partial signature to a regular schnorr. +// This schnorr signature uses a zero value for the 'r' field, so we're just +// only using the last 32-bytes of the signature. This is useful when we need +// to convert an HTLC schnorr signature into something we can send using the +// existing messages. +func (p *MusigPartialSig) ToSchnorrShell() *schnorr.Signature { + var zeroVal btcec.FieldVal + return schnorr.NewSignature(&zeroVal, p.sig.S) +} + +// FromSchnorrShell takes a schnorr signature and parses out the last 32 bytes +// as a normal musig2 partial signature. +func (p *MusigPartialSig) FromSchnorrShell(sig *schnorr.Signature) { + var ( + partialS btcec.ModNScalar + partialSBytes [32]byte + ) + copy(partialSBytes[:], sig.Serialize()[32:]) + partialS.SetBytes(&partialSBytes) + + p.sig = &musig2.PartialSignature{ + S: &partialS, + } +} + +// Verify attempts to verify the partial musig2 signature using the passed +// message and signer public key. +// +// NOTE: This implements the input.Signature interface. +func (p *MusigPartialSig) Verify(msg []byte, pub *btcec.PublicKey) bool { + var m [32]byte + copy(m[:], msg) + + return p.sig.Verify( + p.signerNonce, p.combinedNonce, p.signerKeys, pub, m, + musig2.WithSortedKeys(), musig2.WithBip86SignTweak(), + ) +} + +// MusigNoncePair holds the two nonces needed to sign/verify a new commitment +// state. The signer nonce is the nonce used by the signer (remote nonce), and +// the verification nonce, the nonce used by the verifier (local nonce). +type MusigNoncePair struct { + // SigningNonce is the nonce used by the signer to sign the commitment. + SigningNonce musig2.Nonces + + // VerificationNonce is the nonce used by the verifier to verify the + // commitment. + VerificationNonce musig2.Nonces +} + +// String returns a string representation of the MusigNoncePair. +func (n *MusigNoncePair) String() string { + return fmt.Sprintf("NoncePair(verification_nonce=%x, "+ + "signing_nonce=%x)", n.VerificationNonce.PubNonce[:], + n.SigningNonce.PubNonce[:]) +} + +// MusigSession abstracts over the details of a logical musig session. A single +// session is used for each commitment transactions. The sessions use a JIT +// nonce style, wherein part of the session can be created using only the +// verifier nonce. Once a new state is singed, then the signer nonce is +// generated. similarly, the verify then uses the received signer nonce to +// complete the session and verify the incoming signature. +type MusigSession struct { + // session is the backing musig2 session. We'll use this to interact + // with the musig2 signer. + session *input.MuSig2SessionInfo + + // combinedNonce is the combined nonce of all signers. + combinedNonce [musig2.PubNonceSize]byte + + // nonces is the set of nonces that'll be used to generate/verify the + // next commitment. + nonces MusigNoncePair + + // inputTxOut is the funding input. + inputTxOut *wire.TxOut + + // signerKeys is the set of public keys of all signers. + signerKeys []*btcec.PublicKey + + // remoteKey is the key desc of the remote key. + remoteKey keychain.KeyDescriptor + + // localKey is the key desc of the local key. + localKey keychain.KeyDescriptor + + // signer is the signer that'll be used to interact with the musig + // session. + signer input.MuSig2Signer + + // remoteCommit tracks if this session is for the remote commitment. + remoteCommit bool +} + +// NewPartialMusigSession creates a new musig2 session given only the +// verification nonce (local nonce), and the other information that has already +// been bound to the session. +func NewPartialMusigSession(verificationNonce musig2.Nonces, + localKey, remoteKey keychain.KeyDescriptor, + signer input.MuSig2Signer, inputTxOut *wire.TxOut, + remoteCommit bool) *MusigSession { + + signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey} + + nonces := MusigNoncePair{ + VerificationNonce: verificationNonce, + } + + return &MusigSession{ + nonces: nonces, + remoteKey: remoteKey, + localKey: localKey, + inputTxOut: inputTxOut, + signerKeys: signerKeys, + signer: signer, + remoteCommit: remoteCommit, + } +} + +// FinalizeSession finalizes the session given the signer nonce (remote nonce). +// This is called before signing or verifying a new commitment. +// +// TODO(roasbeef): make private again, add NewMusigSessionthat calls above then +// calls thisd +func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error { + var ( + localNonce, remoteNonce musig2.Nonces + err error + ) + + // First, we'll stash the freshly generated signing nonce. Depending on + // who's commitment we're handling, this'll either be our generated + // nonce, or the one we just got from the remote party. + m.nonces.SigningNonce = signingNonce + + switch { + + // If we're making a session for the remote commitment, then the nonce + // we use to sign is actually will be the signing nonce for the + // session, and their nonce the verification nonce. + case m.remoteCommit: + localNonce = m.nonces.SigningNonce + remoteNonce = m.nonces.VerificationNonce + + // Otherwise, we're generating/receiving a signature for our local + // commitment (to broadcast), so now our verification nonce is the one + // we've already generated, and we want to bind their new signing + // nonce. + default: + localNonce = m.nonces.VerificationNonce + remoteNonce = m.nonces.SigningNonce + } + + tweakDesc := input.MuSig2Tweaks{ + TaprootBIP0086Tweak: true, + } + m.session, err = m.signer.MuSig2CreateSession( + input.MuSig2Version100RC2, m.localKey.KeyLocator, m.signerKeys, + &tweakDesc, [][musig2.PubNonceSize]byte{remoteNonce.PubNonce}, + musig2.WithPreGeneratedNonce(&localNonce), + ) + if err != nil { + return err + } + + // We'll need the raw combined nonces later to be able to verify + // partial signatures, and also combine partial signatures, so we'll + // generate it now ourselves. + m.combinedNonce, err = musig2.AggregateNonces([][musig2.PubNonceSize]byte{ + m.nonces.SigningNonce.PubNonce, + m.nonces.VerificationNonce.PubNonce, + }) + if err != nil { + return nil + } + + return nil +} + +// taprootKeyspendSighash generates the sighash for a taproot key spend. As +// this is a musig2 channel output, the keyspend is the only path we can take. +func taprootKeyspendSighash(tx *wire.MsgTx, pkScript []byte, + value int64) ([]byte, error) { + + prevOutputFetcher := txscript.NewCannedPrevOutputFetcher( + pkScript, value, + ) + + sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) + + return txscript.CalcTaprootSignatureHash( + sigHashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher, + ) +} + +// SignCommit signs the passed commitment w/ the current signing (relative +// remote) nonce. Given nonces should only ever be used once, once the method +// returns a new nonce is returned, w/ the existing nonce blanked out. +func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) { + // If we already have a session, then we don't need to finalize as this + // was done up front (symmetric nonce case, like for co-op close). + if m.session == nil { + // Before we can sign a new commitment, we'll need to generate + // a fresh nonce that'll be sent along side our signature. With + // the nonce in hand, we can finalize the session. + // + // TODO(roasbeef): can also pass in stuff like the sighash to + // further bind context, etc, etc. + signingNonce, err := musig2.GenNonces( + musig2.WithPublicKey(m.localKey.PubKey), + ) + if err != nil { + return nil, err + } + if err := m.FinalizeSession(*signingNonce); err != nil { + return nil, err + } + } + + // Once we sign with a nonce, we'll never use it again, so it's safe to + // go ahead and clean up the session right here. + // defer m.signer.MuSig2Cleanup(m.session.SessionID) + // + // TODO(roasbeef): can't clean up here as need to combine sig + + // Next we can sign, we'll need to generate the sighash for their + // commitment transaction. + sigHash, err := taprootKeyspendSighash( + tx, m.inputTxOut.PkScript, m.inputTxOut.Value, + ) + if err != nil { + return nil, err + } + + // Now that we have our session created, we'll use it to generate the + // initial partial signature over our sighash. + var sigHashMsg [32]byte + copy(sigHashMsg[:], sigHash) + + walletLog.Infof("Generating new musig2 sig for session=%x, nonces=%s", + m.session.SessionID[:], m.nonces.String()) + + sig, err := m.signer.MuSig2Sign( + m.session.SessionID, sigHashMsg, false, + ) + if err != nil { + return nil, err + } + + return NewMusigPartialSig( + sig, m.session.PublicNonce, m.combinedNonce, m.signerKeys, + ), nil +} + +// Refresh is called once we receive a new verification nonce from the remote +// party after sending a signature. This nonce will be coupled within the +// revoke-and-ack message of the remote party. +func (m *MusigSession) Refresh(verificationNonce *musig2.Nonces, +) (*MusigSession, error) { + + return NewPartialMusigSession( + *verificationNonce, m.localKey, m.remoteKey, m.signer, + m.inputTxOut, m.remoteCommit, + ), nil +} + +// VerificationNonce returns the current verification nonce for the session. +func (m *MusigSession) VerificationNonce() *musig2.Nonces { + return &m.nonces.VerificationNonce +} + +// musigSessionOpts is a set of options that can be used to modify calls to the +// musig session. +type musigSessionOpts struct { + // customRand is an optional custom random source that can be used to + // generate nonces via a counter scheme. + customRand io.Reader +} + +// defaultMusigSessionOpts returns the default set of options for the musig +// session. +func defaultMusigSessionOpts() *musigSessionOpts { + return &musigSessionOpts{} +} + +// MusigSessionOpt is a functional option that can be used to modify calls to +// the musig session. +type MusigSessionOpt func(*musigSessionOpts) + +// WithLocalCounterNonce is used to generate local nonces based on the shachain +// producer and the current height. This allows us to not have to write secret +// nonce state to disk. Instead, we can use this to derive the nonce we need to +// sign and broadcast our own commitment transaction. +func WithLocalCounterNonce(targetHeight uint64, + shaGen shachain.Producer) MusigSessionOpt { + + return func(opt *musigSessionOpts) { + nextPreimage, _ := shaGen.AtIndex(targetHeight) + + opt.customRand = bytes.NewBuffer(nextPreimage[:]) + } +} + +// VerifyCommitSig attempts to verify the passed partial signature against the +// passed commitment transaction. A keyspend sighash is assumed to generate the +// signed message. As we never re-use nonces, a new verification nonce (our +// relative local nonce) returned to transmit to the remote party, which allows +// them to generate another signature. +func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx, + sig *lnwire.PartialSigWithNonce, + musigOpts ...MusigSessionOpt) (*musig2.Nonces, error) { + + opts := defaultMusigSessionOpts() + for _, optFunc := range musigOpts { + optFunc(opts) + } + + // Before we can verify the signature, we'll need to finalize the + // session by binding the remote party's provided signing nonce. + if err := m.FinalizeSession(musig2.Nonces{ + PubNonce: sig.Nonce, + }); err != nil { + return nil, err + } + + // When we verify a commitment signature, we always assume that we're + // verifying a signature on our local commitment. Therefore, we'll use: + // their remote nonce, and also public key. + partialSig := NewMusigPartialSig( + &musig2.PartialSignature{S: &sig.Sig}, + m.nonces.SigningNonce.PubNonce, m.combinedNonce, m.signerKeys, + ) + + walletLog.Infof("verify partial sig: %v", spew.Sdump(partialSig)) + + // With the partial sig loaded with the proper context, we'll now + // generate the sighash that the remote party should have signed. + sigHash, err := taprootKeyspendSighash( + commitTx, m.inputTxOut.PkScript, m.inputTxOut.Value, + ) + if err != nil { + return nil, err + } + + walletLog.Infof("Verfiying new musig2 sig for session=%x, nonce=%s", + m.session.SessionID[:], m.nonces.String()) + + if !partialSig.Verify(sigHash, m.remoteKey.PubKey) { + return nil, fmt.Errorf("invalid partial commit sig") + } + + nonceOpts := []musig2.NonceGenOption{ + musig2.WithPublicKey(m.localKey.PubKey), + } + if opts.customRand != nil { + nonceOpts = append( + nonceOpts, musig2.WithCustomRand(opts.customRand), + ) + } + + // At this point, we know that their signature is valid, so we'll + // generate another verification nonce for them, so they can generate a + // new state transition. + nextVerificationNonce, err := musig2.GenNonces(nonceOpts...) + if err != nil { + return nil, fmt.Errorf("unable to gen new nonce: %w", err) + } + + return nextVerificationNonce, nil +} + +// CombineSigs combines the passed partial signatures into a valid schnorr +// signature. +func (m *MusigSession) CombineSigs(sigs ...*musig2.PartialSignature, +) (*schnorr.Signature, error) { + + defer m.signer.MuSig2Cleanup(m.session.SessionID) + + sig, _, err := m.signer.MuSig2CombineSig( + m.session.SessionID, + sigs, + ) + if err != nil { + return nil, err + } + + return sig, nil +} + +// MusigSessionCfg is used to create a new musig2 pair session. It contains the +// keys for both parties, as well as their initial verification nonces. +type MusigSessionCfg struct { + // LocalKey is a key desc for the local key. + LocalKey keychain.KeyDescriptor + + // RemoteKey is a key desc for the remote key. + RemoteKey keychain.KeyDescriptor + + // LocalNonce is the local party's initial verification nonce. + LocalNonce musig2.Nonces + + // RemoteNonce is the remote party's initial verification nonce. + RemoteNonce musig2.Nonces + + // Signer is the signer that will be used to generate the session. + Signer input.MuSig2Signer + + // InputTxOut is the output that we're signing for. This will be the + // funding input. + InputTxOut *wire.TxOut +} + +// MusigPairSession houses the two musig2 sessions needed to do funding and +// drive forward the state machine. The local session is used to verify +// incoming commitment states. The remote session is used to propose new +// commitment states to the remote party. +type MusigPairSession struct { + // LocalSession is the local party's musig2 session. + LocalSession *MusigSession + + // RemoteSession is the remote party's musig2 session. + RemoteSession *MusigSession + + // signer is the signer that will be used to drive the session. + signer input.MuSig2Signer +} + +// NewMusigPairSession creates a new musig2 pair session. +func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession { + // Given the config passed in, we'll now create our two sessions: one + // for the local commit, and one for the remote commit. + // + // Both sessions will be created using only the verification nonce for + // the local+remote party. + localSession := NewPartialMusigSession( + cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, + cfg.Signer, cfg.InputTxOut, false, + ) + remoteSession := NewPartialMusigSession( + cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, + cfg.Signer, cfg.InputTxOut, true, + ) + + return &MusigPairSession{ + LocalSession: localSession, + RemoteSession: remoteSession, + signer: cfg.Signer, + } +}