lnd/lnwallet/musig_session.go
Olaoluwa Osuntokun ce93b236aa
input+lnwallet: modify musig2 interfaces use explicit optional local nonces
In this commit, we modify the musig2 interfaces to instead use an
explicit value for the local nonces. Before this commit, we used the
functional option, but we want to also support specifying this value
over RPC for the remote signer. The functional option pattern is opaque,
so we can't get the nonce value we need. To get around this, we'll just
make this an explicit pointer, then map this to the functional option at
the very last moment.
2023-09-18 11:42:13 -07:00

579 lines
18 KiB
Go

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/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/shachain"
)
// MusigCommitType is an enum that denotes if this is the local or remote
// commitment.
type MusigCommitType uint8
const (
// LocalMusigCommit denotes that this a session for the local
// commitment.
LocalMusigCommit MusigCommitType = iota
// RemoteMusigCommit denotes that this is a session for the remote
// commitment.
RemoteMusigCommit
)
var (
// ErrSessionNotFinalized is returned when the SignCommit method is
// called for a local commitment, without the session being finalized
// (missing nonce).
ErrSessionNotFinalized = fmt.Errorf("musig2 session not finalized")
)
// 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 lnwire.Musig2Nonce
// combinedNonce is the combined nonce of all signers.
combinedNonce lnwire.Musig2Nonce
// 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 lnwire.Musig2Nonce,
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 signed, then the signer nonce is
// generated. Similarly, the verifier 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 lnwire.Musig2Nonce
// 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
// commitType tracks if this is the session for the local or remote
// commitment.
commitType MusigCommitType
}
// 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,
commitType MusigCommitType) *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,
commitType: commitType,
}
}
// FinalizeSession finalizes the session given the signer nonce. This is
// called before signing or verifying a new commitment.
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 m.commitType {
// 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 RemoteMusigCommit:
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.
case LocalMusigCommit:
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},
&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.
aggNonce, err := musig2.AggregateNonces([][musig2.PubNonceSize]byte{
m.nonces.SigningNonce.PubNonce,
m.nonces.VerificationNonce.PubNonce,
})
if err != nil {
return nil
}
m.combinedNonce = aggNonce
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) {
switch {
// 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).
case m.session == nil && m.commitType == RemoteMusigCommit:
// 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.
txHash := tx.TxHash()
signingNonce, err := musig2.GenNonces(
musig2.WithPublicKey(m.localKey.PubKey),
musig2.WithNonceAuxInput(txHash[:]),
)
if err != nil {
return nil, err
}
if err := m.FinalizeSession(*signingNonce); err != nil {
return nil, err
}
// Otherwise, we're trying to make a new commitment transaction without
// an active session, so we'll error out.
case m.session == nil:
return nil, ErrSessionNotFinalized
}
// 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.commitType,
), 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[:])
}
}
// invalidPartialSigError is used to return additional debug information to a
// caller that encounters an invalid partial sig.
type invalidPartialSigError struct {
partialSig []byte
sigHash []byte
signingNonce [musig2.PubNonceSize]byte
verificationNonce [musig2.PubNonceSize]byte
}
// Error returns the error string for the partial sig error.
func (i invalidPartialSigError) Error() string {
return fmt.Sprintf("invalid partial sig: partial_sig=%x, "+
"sig_hash=%x, signing_nonce=%x, verification_nonce=%x",
i.partialSig, i.sigHash, i.signingNonce[:],
i.verificationNonce[:])
}
// 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)
}
if sig == nil {
return nil, fmt.Errorf("sig not provided")
}
// 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,
)
// 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("Verifying new musig2 sig for session=%x, nonce=%s",
m.session.SessionID[:], m.nonces.String())
if partialSig == nil {
return nil, fmt.Errorf("partial sig not set")
}
if !partialSig.Verify(sigHash, m.remoteKey.PubKey) {
return nil, &invalidPartialSigError{
partialSig: partialSig.Serialize(),
sigHash: sigHash,
verificationNonce: m.nonces.VerificationNonce.PubNonce,
signingNonce: m.nonces.SigningNonce.PubNonce,
}
}
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) {
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, LocalMusigCommit,
)
remoteSession := NewPartialMusigSession(
cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey,
cfg.Signer, cfg.InputTxOut, RemoteMusigCommit,
)
return &MusigPairSession{
LocalSession: localSession,
RemoteSession: remoteSession,
signer: cfg.Signer,
}
}