Skip to content

Commit 37e656c

Browse files
committed
musig2: add WithExternalCombinedNonce option to Sign
This commits adds a new functional option to the Session.Sign function which allows specifying an external combined Nonce. This is useful when a central coordinator aggregates all nonces.
1 parent b7d0706 commit 37e656c

File tree

3 files changed

+113
-6
lines changed

3 files changed

+113
-6
lines changed

btcec/schnorr/musig2/context.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -554,15 +554,31 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) {
554554
func (s *Session) Sign(msg [32]byte,
555555
signOpts ...SignOption) (*PartialSignature, error) {
556556

557-
switch {
558557
// If no local nonce is present, then this means we already signed, so
559558
// we'll return an error to prevent nonce re-use.
560-
case s.localNonces == nil:
559+
if s.localNonces == nil {
561560
return nil, ErrSigningContextReuse
561+
}
562+
563+
opts := defaultSignOptions()
564+
for _, opt := range signOpts {
565+
opt(opts)
566+
}
562567

563-
// We also need to make sure we have the combined nonce, otherwise this
564-
// function was called too early.
565-
case s.combinedNonce == nil:
568+
// Determine which combined nonce to use: external or internal.
569+
var combinedNonce *[PubNonceSize]byte
570+
switch {
571+
case opts.externalCombinedNonce != nil:
572+
// Use the externally provided combined nonce from the
573+
// coordinator.
574+
combinedNonce = opts.externalCombinedNonce
575+
576+
case s.combinedNonce != nil:
577+
// Use the internally aggregated combined nonce.
578+
combinedNonce = s.combinedNonce
579+
580+
default:
581+
// Neither external nor internal combined nonce is available.
566582
return nil, ErrCombinedNonceUnavailable
567583
}
568584

@@ -580,7 +596,7 @@ func (s *Session) Sign(msg [32]byte,
580596
}
581597

582598
partialSig, err := Sign(
583-
s.localNonces.SecNonce, s.ctx.signingKey, *s.combinedNonce,
599+
s.localNonces.SecNonce, s.ctx.signingKey, *combinedNonce,
584600
s.ctx.opts.keySet, msg, signOpts...,
585601
)
586602

btcec/schnorr/musig2/musig2_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,79 @@ func (mr *memsetRandReader) Read(buf []byte) (n int, err error) {
439439
}
440440
return len(buf), nil
441441
}
442+
443+
// TestSigningWithAggregatedNonce tests the aggregated nonce signing flow where
444+
// a coordinator aggregates nonces and distributes the combined nonce to
445+
// participants, rather than each participant aggregating nonces themselves.
446+
func TestSigningWithAggregatedNonce(t *testing.T) {
447+
const numSigners = 5
448+
449+
// Generate signers.
450+
signerKeys := make([]*btcec.PrivateKey, numSigners)
451+
signSet := make([]*btcec.PublicKey, numSigners)
452+
for i := 0; i < numSigners; i++ {
453+
privKey, err := btcec.NewPrivateKey()
454+
if err != nil {
455+
t.Fatalf("unable to gen priv key: %v", err)
456+
}
457+
signerKeys[i] = privKey
458+
signSet[i] = privKey.PubKey()
459+
}
460+
461+
// Each signer creates a context and session.
462+
sessions := make([]*Session, numSigners)
463+
for i, signerKey := range signerKeys {
464+
signCtx, err := NewContext(
465+
signerKey, false, WithKnownSigners(signSet),
466+
)
467+
if err != nil {
468+
t.Fatalf("unable to generate context: %v", err)
469+
}
470+
471+
session, err := signCtx.NewSession()
472+
if err != nil {
473+
t.Fatalf("unable to generate new session: %v", err)
474+
}
475+
sessions[i] = session
476+
}
477+
478+
// Phase 1: Coordinator collects all public nonces.
479+
pubNonces := make([][PubNonceSize]byte, numSigners)
480+
for i, session := range sessions {
481+
pubNonces[i] = session.PublicNonce()
482+
}
483+
484+
// Phase 2: Coordinator aggregates nonces.
485+
combinedNonce, err := AggregateNonces(pubNonces)
486+
if err != nil {
487+
t.Fatalf("unable to aggregate nonces: %v", err)
488+
}
489+
490+
// Phase 3: Coordinator distributes combined nonce to all participants.
491+
// Participants sign using the external combined nonce.
492+
msg := sha256.Sum256([]byte("coordinator-based signing"))
493+
494+
partialSigs := make([]*PartialSignature, numSigners)
495+
for i, session := range sessions {
496+
// Participants use WithExternalCombinedNonce instead of
497+
// RegisterPubNonce.
498+
sig, err := session.Sign(msg, WithExternalCombinedNonce(combinedNonce))
499+
if err != nil {
500+
t.Fatalf("signer %d unable to sign: %v", i, err)
501+
}
502+
partialSigs[i] = sig
503+
}
504+
505+
// Phase 4: Combine all partial signatures (can be done by any party).
506+
finalSig := CombineSigs(partialSigs[0].R, partialSigs)
507+
508+
// Verify the final signature.
509+
combinedKey, _, _, err := AggregateKeys(signSet, false)
510+
if err != nil {
511+
t.Fatalf("unable to aggregate keys: %v", err)
512+
}
513+
514+
if !finalSig.Verify(msg[:], combinedKey.FinalKey) {
515+
t.Fatalf("final signature is invalid")
516+
}
517+
}

btcec/schnorr/musig2/sign.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ type signOptions struct {
134134
// 86 style, where we don't expect an actual tweak and instead just
135135
// commit to the public key itself.
136136
bip86Tweak bool
137+
138+
// externalCombinedNonce allows a caller to provide a pre-aggregated
139+
// combined nonce.
140+
externalCombinedNonce *[PubNonceSize]byte
137141
}
138142

139143
// defaultSignOptions returns the default set of signing operations.
@@ -195,6 +199,17 @@ func WithBip86SignTweak() SignOption {
195199
}
196200
}
197201

202+
// WithExternalCombinedNonce allows a caller to specify a pre-aggregated
203+
// combined nonce for signing. This is useful in coordinator-based signing
204+
// protocols where a coordinator collects all individual public nonces,
205+
// aggregates them, and then distributes the combined nonce to all participants.
206+
func WithExternalCombinedNonce(combinedNonce [PubNonceSize]byte) SignOption {
207+
return func(o *signOptions) {
208+
nonceCopy := combinedNonce
209+
o.externalCombinedNonce = &nonceCopy
210+
}
211+
}
212+
198213
// computeSigningNonce calculates the final nonce used for signing. This will
199214
// be the R value used in the final signature.
200215
func computeSigningNonce(combinedNonce [PubNonceSize]byte,

0 commit comments

Comments
 (0)