From cb22c9db6c346ad901369948a09223b5941121e2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 30 Jun 2022 22:17:34 +0200 Subject: [PATCH] oder+rpcserver: add MuSig2 batch signing --- order/batch.go | 26 +++++++++--- order/batch_signer.go | 85 ++++++++++++++++++++++++++++++++++------ order/interfaces.go | 20 +++++----- order/manager.go | 10 ++--- order/mock_interfaces.go | 7 ++-- order/rpc_parse.go | 43 ++++++++++++++++++++ rpcserver.go | 32 +++++++++++++-- 7 files changed, 186 insertions(+), 37 deletions(-) diff --git a/order/batch.go b/order/batch.go index 70e858550..ac68d23cc 100644 --- a/order/batch.go +++ b/order/batch.go @@ -8,7 +8,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" @@ -239,6 +239,18 @@ type Batch struct { // which the batch transaction can be found within. This will be used by // traders to base off their absolute channel lease maturity height. HeightHint uint32 + + // ServerNonces is the map of all the auctioneer's public nonces for + // each of the (Taproot enabled) accounts in the batch, keyed by the + // account's trader key. This is volatile (in-memory only) information + // that is _not_ persisted as part of the batch snapshot. + ServerNonces AccountNonces + + // PreviousOutputs is the list of previous output scripts and amounts + // (UTXO information) for all the inputs being spent by the batch + // transaction. This is volatile (in-memory only) information that is + // _not_ persisted as part of the batch snapshot. + PreviousOutputs []*wire.TxOut } // Fetcher describes a function that's able to fetch the latest version of an @@ -332,9 +344,13 @@ type MatchedOrder struct { } // BatchSignature is a map type that is keyed by a trader's account key and -// contains the multi-sig signature for the input that -// spends from the current account in a batch. -type BatchSignature map[[33]byte]*ecdsa.Signature +// contains the multi-sig signature for the input that spends from the current +// account in a batch. +type BatchSignature map[[33]byte][]byte + +// AccountNonces is a map of all server or client nonces for a batch signing +// session, keyed by the account key. +type AccountNonces map[[33]byte][musig2.PubNonceSize]byte // BatchVerifier is an interface that can verify a batch from the point of view // of the trader. @@ -349,7 +365,7 @@ type BatchVerifier interface { type BatchSigner interface { // Sign returns the witness stack of all account inputs in a batch that // belong to the trader. - Sign(*Batch) (BatchSignature, error) + Sign(*Batch) (BatchSignature, AccountNonces, error) } // BatchStorer is an interface that can store a batch to the local database by diff --git a/order/batch_signer.go b/order/batch_signer.go index 7d48d967d..94c3266d5 100644 --- a/order/batch_signer.go +++ b/order/batch_signer.go @@ -6,6 +6,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/txscript" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/pool/account" @@ -23,8 +24,9 @@ type batchSigner struct { // belong to the trader. // // NOTE: This method is part of the BatchSigner interface. -func (s *batchSigner) Sign(batch *Batch) (BatchSignature, error) { - ourSigs := make(BatchSignature) +func (s *batchSigner) Sign(batch *Batch) (BatchSignature, AccountNonces, error) { + ourSigs := make(BatchSignature, len(batch.AccountDiffs)) + ourNonces := make(AccountNonces, len(batch.AccountDiffs)) // At this point we know that the accounts charged are correct. So we // can just go through them, find the corresponding input in the batch @@ -33,12 +35,13 @@ func (s *batchSigner) Sign(batch *Batch) (BatchSignature, error) { // Get account from DB and make sure we can create the output. acct, err := s.getAccount(acctDiff.AccountKey) if err != nil { - return nil, fmt.Errorf("account not found: %v", err) + return nil, nil, fmt.Errorf("account not found: %v", + err) } acctOut, err := acct.Output() if err != nil { - return nil, fmt.Errorf("could not get account output: "+ - "%v", err) + return nil, nil, fmt.Errorf("could not get account "+ + "output: %v", err) } var acctKey [33]byte copy(acctKey[:], acct.TraderKey.PubKey.SerializeCompressed()) @@ -51,7 +54,24 @@ func (s *batchSigner) Sign(batch *Batch) (BatchSignature, error) { } } if inputIndex == -1 { - return nil, fmt.Errorf("account input not found") + return nil, nil, fmt.Errorf("account input not found") + } + + // MuSig2 signing works differently! + if acct.Version >= account.VersionTaprootEnabled { + partialSig, nonces, err := s.signInputMuSig2( + acctKey, acct, inputIndex, batch, + ) + if err != nil { + return nil, nil, fmt.Errorf("error MuSig2 "+ + "signing input %d: %v", inputIndex, err) + } + + ourSigs[acctKey] = partialSig + ourNonces[acctKey] = nonces + + // We're done signing for this account input. + continue } // Gather the remaining components required to sign the @@ -64,7 +84,7 @@ func (s *batchSigner) Sign(batch *Batch) (BatchSignature, error) { acct.BatchKey, acct.Secret, ) if err != nil { - return nil, err + return nil, nil, err } signDesc := &lndclient.SignDescriptor{ KeyDesc: *acct.TraderKey, @@ -79,15 +99,58 @@ func (s *batchSigner) Sign(batch *Batch) (BatchSignature, error) { []*lndclient.SignDescriptor{signDesc}, nil, ) if err != nil { - return nil, err + return nil, nil, err } - ourSigs[acctKey], err = ecdsa.ParseDERSignature(sigs[0]) + + // Make sure the signature is in the expected format (mostly a + // precaution). + _, err = ecdsa.ParseDERSignature(sigs[0]) if err != nil { - return nil, err + return nil, nil, err } + + ourSigs[acctKey] = sigs[0] + } + + return ourSigs, ourNonces, nil +} + +// signInputMuSig2 creates a MuSig2 partial signature for the given account's +// input. +func (s *batchSigner) signInputMuSig2(acctKey [33]byte, + account *account.Account, accountInputIdx int, batch *Batch) ([]byte, + [musig2.PubNonceSize]byte, error) { + + ctx := context.Background() + emptyNonce := [musig2.PubNonceSize]byte{} + + serverNonces, ok := batch.ServerNonces[acctKey] + if !ok { + return nil, emptyNonce, fmt.Errorf("server didn't include "+ + "nonces for account %x", acctKey[:]) + } + + sessionInfo, cleanup, err := poolscript.TaprootMuSig2SigningSession( + ctx, account.Expiry, account.TraderKey.PubKey, account.BatchKey, + account.Secret, account.AuctioneerKey, s.signer, + &account.TraderKey.KeyLocator, &serverNonces, + ) + if err != nil { + return nil, emptyNonce, fmt.Errorf("error creating MuSig2 "+ + "session: %v", err) + } + + partialSig, err := poolscript.TaprootMuSig2Sign( + ctx, accountInputIdx, sessionInfo, s.signer, batch.BatchTX, + batch.PreviousOutputs, nil, nil, + ) + if err != nil { + cleanup() + return nil, emptyNonce, fmt.Errorf("error signing batch TX: %v", + err) } - return ourSigs, nil + return partialSig, sessionInfo.PublicNonce, nil } // A compile-time constraint to ensure batchSigner implements BatchSigner. diff --git a/order/interfaces.go b/order/interfaces.go index 6d08b5b0a..a602abacb 100644 --- a/order/interfaces.go +++ b/order/interfaces.go @@ -862,24 +862,26 @@ type Manager interface { PrepareOrder(ctx context.Context, order Order, acct *account.Account, terms *terms.AuctioneerTerms) (*ServerOrderParams, error) - // OrderMatchValidate verifies an incoming batch is sane before accepting it. + // OrderMatchValidate verifies an incoming batch is sane before + // accepting it. OrderMatchValidate(batch *Batch, bestHeight uint32) error - // HasPendingBatch returns whether a pending batch is currently being processed. + // HasPendingBatch returns whether a pending batch is currently being + // processed. HasPendingBatch() bool // PendingBatch returns the current pending batch being validated. PendingBatch() *Batch - // BatchSign returns the witness stack of all account inputs in a batch that - // belong to the trader. - BatchSign() (BatchSignature, error) + // BatchSign returns the witness stack of all account inputs in a batch + // that belong to the trader. + BatchSign() (BatchSignature, AccountNonces, error) - // BatchFinalize marks a batch as complete upon receiving the finalize message - // from the auctioneer. + // BatchFinalize marks a batch as complete upon receiving the finalize + // message from the auctioneer. BatchFinalize(batchID BatchID) error - // OurNodePubkey returns our lnd node's public identity key or an error if the - // manager wasn't fully started yet. + // OurNodePubkey returns our lnd node's public identity key or an error + // if the manager wasn't fully started yet. OurNodePubkey() ([33]byte, error) } diff --git a/order/manager.go b/order/manager.go index c9d5937e2..c81014722 100644 --- a/order/manager.go +++ b/order/manager.go @@ -383,18 +383,18 @@ func (m *manager) PendingBatch() *Batch { // belong to the trader. Before sending off the signature to the auctioneer, // we'll also persist the batch to disk as pending to ensure we can recover // after a crash. -func (m *manager) BatchSign() (BatchSignature, error) { - sig, err := m.batchSigner.Sign(m.pendingBatch) +func (m *manager) BatchSign() (BatchSignature, AccountNonces, error) { + sig, nonces, err := m.batchSigner.Sign(m.pendingBatch) if err != nil { - return nil, err + return nil, nil, err } err = m.batchStorer.StorePendingBatch(m.pendingBatch) if err != nil { - return nil, fmt.Errorf("unable to store batch: %v", err) + return nil, nil, fmt.Errorf("unable to store batch: %v", err) } - return sig, nil + return sig, nonces, nil } // BatchFinalize marks a batch as complete upon receiving the finalize message diff --git a/order/mock_interfaces.go b/order/mock_interfaces.go index 83d1422ce..8722a374a 100644 --- a/order/mock_interfaces.go +++ b/order/mock_interfaces.go @@ -288,12 +288,13 @@ func (mr *MockManagerMockRecorder) BatchFinalize(batchID interface{}) *gomock.Ca } // BatchSign mocks base method. -func (m *MockManager) BatchSign() (BatchSignature, error) { +func (m *MockManager) BatchSign() (BatchSignature, AccountNonces, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BatchSign") ret0, _ := ret[0].(BatchSignature) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(AccountNonces) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // BatchSign indicates an expected call of BatchSign. diff --git a/order/rpc_parse.go b/order/rpc_parse.go index a86f739bb..347c7abea 100644 --- a/order/rpc_parse.go +++ b/order/rpc_parse.go @@ -9,6 +9,7 @@ import ( "net" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/pool/account" @@ -465,6 +466,48 @@ func ParseRPCMatchedOrders(orders *auctioneerrpc.MatchedOrder) ([]*MatchedOrder, return result, nil } +// ParseRPCSign parses the incoming raw OrderMatchSignBegin into the go native +// structs used by the order manager. +func ParseRPCSign(signMsg *auctioneerrpc.OrderMatchSignBegin) (AccountNonces, + []*wire.TxOut, error) { + + nonces := make(AccountNonces, len(signMsg.ServerNonces)) + for acctKeyHex, nonceBytes := range signMsg.ServerNonces { + var acctKey [btcec.PubKeyBytesLenCompressed]byte + + if len(acctKeyHex) != hex.EncodedLen(len(acctKey)) { + return nil, nil, fmt.Errorf("invalid account key " + + "length in server nonces") + } + if len(nonceBytes) != musig2.PubNonceSize { + return nil, nil, fmt.Errorf("invalid pub nonce " + + "length in server nonces") + } + + acctKeyBytes, err := hex.DecodeString(acctKeyHex) + if err != nil { + return nil, nil, fmt.Errorf("error hex decoding "+ + "account key: %v", err) + } + copy(acctKey[:], acctKeyBytes) + + var pubNonces [musig2.PubNonceSize]byte + copy(pubNonces[:], nonceBytes) + + nonces[acctKey] = pubNonces + } + + prevOutputs := make([]*wire.TxOut, len(signMsg.PrevOutputs)) + for idx, rpcPrevOut := range signMsg.PrevOutputs { + prevOutputs[idx] = &wire.TxOut{ + Value: int64(rpcPrevOut.Value), + PkScript: rpcPrevOut.PkScript, + } + } + + return nonces, prevOutputs, nil +} + // randomPreimage creates a new preimage from a random number generator. func randomPreimage() ([]byte, error) { var nonce Nonce diff --git a/rpcserver.go b/rpcserver.go index 33df5eaf1..356e920c5 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -387,10 +387,22 @@ func (s *rpcServer) handleServerMessage( } case *auctioneerrpc.ServerAuctionMessage_Sign: + batch := s.orderManager.PendingBatch() + + // There is some auxiliary information in the "sign" message + // that we need for the MuSig2/Taproot signing, let's try to + // parse that first. + serverNonces, prevOutputs, err := order.ParseRPCSign(msg.Sign) + if err != nil { + rpcLog.Errorf("Error parsing sign aux info: %v", err) + return s.sendRejectBatch(batch, err) + } + batch.ServerNonces = serverNonces + batch.PreviousOutputs = prevOutputs + // We were able to accept the batch. Inform the auctioneer, // then start negotiating with the remote peers. We'll sign // once all channel partners have responded. - batch := s.orderManager.PendingBatch() channelKeys, err := s.server.fundingManager.BatchChannelSetup( batch, ) @@ -403,12 +415,12 @@ func (s *rpcServer) handleServerMessage( "num_orders=%v", batch.ID[:], len(batch.MatchedOrders)) // Sign for the accounts in the batch. - sigs, err := s.orderManager.BatchSign() + sigs, nonces, err := s.orderManager.BatchSign() if err != nil { rpcLog.Errorf("Error signing batch: %v", err) return s.sendRejectBatch(batch, err) } - err = s.sendSignBatch(batch, sigs, channelKeys) + err = s.sendSignBatch(batch, sigs, nonces, channelKeys) if err != nil { rpcLog.Errorf("Error sending sign msg: %v", err) return s.sendRejectBatch(batch, err) @@ -1913,6 +1925,7 @@ func (s *rpcServer) GetLsatTokens(_ context.Context, // sendSignBatch sends a sign message to the server with the witness stacks of // all accounts that are involved in the batch. func (s *rpcServer) sendSignBatch(batch *order.Batch, sigs order.BatchSignature, + nonces order.AccountNonces, chanInfos map[wire.OutPoint]*chaninfo.ChannelInfo) error { // Prepare the list of witness stacks and channel infos and send them to @@ -1920,7 +1933,17 @@ func (s *rpcServer) sendSignBatch(batch *order.Batch, sigs order.BatchSignature, rpcSigs := make(map[string][]byte, len(sigs)) for acctKey, sig := range sigs { key := hex.EncodeToString(acctKey[:]) - rpcSigs[key] = sig.Serialize() + rpcSigs[key] = make([]byte, len(sig)) + copy(rpcSigs[key], sig) + } + + // Prepare the trader's nonces too (if there are any Taproot/MuSig2 + // accounts in the batch). + rpcNonces := make(map[string][]byte, len(nonces)) + for acctKey, nonce := range nonces { + key := hex.EncodeToString(acctKey[:]) + rpcNonces[key] = make([]byte, 66) + copy(rpcNonces[key], nonce[:]) } rpcChannelInfos, err := marshallChannelInfo(chanInfos) @@ -1943,6 +1966,7 @@ func (s *rpcServer) sendSignBatch(batch *order.Batch, sigs order.BatchSignature, Sign: &auctioneerrpc.OrderMatchSign{ BatchId: batch.ID[:], AccountSigs: rpcSigs, + TraderNonces: rpcNonces, ChannelInfos: rpcChannelInfos, }, },