Skip to content

Commit

Permalink
oder+rpcserver: add MuSig2 batch signing
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Jul 1, 2022
1 parent 94a0fe7 commit cb22c9d
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 37 deletions.
26 changes: 21 additions & 5 deletions order/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
85 changes: 74 additions & 11 deletions order/batch_signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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())
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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.
Expand Down
20 changes: 11 additions & 9 deletions order/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
10 changes: 5 additions & 5 deletions order/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions order/mock_interfaces.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions order/rpc_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
32 changes: 28 additions & 4 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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)
Expand Down Expand Up @@ -1913,14 +1925,25 @@ 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
// the server.
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)
Expand All @@ -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,
},
},
Expand Down

0 comments on commit cb22c9d

Please sign in to comment.