From 8f54cc6edab2d4f9348098c60c2d477c2950bbda Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Tue, 7 Oct 2025 20:25:39 +0200 Subject: [PATCH 1/2] musig2: add Session.RegisterCombinedNonce This commit adds a new function to musig2.Session, which allows the caller to add an external aggregated nonce to the session. --- btcec/schnorr/musig2/context.go | 44 +++- btcec/schnorr/musig2/musig2_test.go | 323 ++++++++++++++++++++++++++++ 2 files changed, 366 insertions(+), 1 deletion(-) diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index 8e6b7154d3..54c0a43d68 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -59,6 +59,11 @@ var ( // ErrNotEnoughSigners is returned if a caller attempts to obtain an // early nonce when it wasn't specified ErrNoEarlyNonce = fmt.Errorf("no early nonce available") + + // ErrCombinedNonceAfterPubNonces is returned if RegisterCombinedNonce + // is called after public nonces have already been registered. + ErrCombinedNonceAfterPubNonces = fmt.Errorf("can't register combined " + + "nonce after public nonces") ) // Context is a managed signing context for musig2. It takes care of things @@ -525,7 +530,7 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { // If we already have all the nonces, then this method was called too // many times. haveAllNonces := len(s.pubNonces) == s.ctx.opts.numSigners - if haveAllNonces { + if haveAllNonces || s.combinedNonce != nil { return false, ErrAlredyHaveAllNonces } @@ -548,6 +553,43 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { return haveAllNonces, nil } +// RegisterCombinedNonce allows a caller to directly register a combined nonce +// that was generated externally. This is useful in coordinator-based +// protocols where the coordinator aggregates all nonces and distributes the +// combined nonce to participants, rather than each participant aggregating +// nonces themselves. +func (s *Session) RegisterCombinedNonce( + combinedNonce [PubNonceSize]byte) error { + + // If we already have a combined nonce, then this method was called too + // many times. + if s.combinedNonce != nil { + return ErrAlredyHaveAllNonces + } + + // We also don't allow this method to be called if we already registered + // some public nonces. + if len(s.pubNonces) > 1 { + return ErrCombinedNonceAfterPubNonces + } + + // We'll now try to parse the combined nonce into it's two points to + // ensure it's valid. + _, err := btcec.ParsePubKey(combinedNonce[:33]) + if err != nil { + return fmt.Errorf("invalid combined nonce: %w", err) + } + _, err = btcec.ParsePubKey(combinedNonce[33:]) + if err != nil { + return fmt.Errorf("invalid combined nonce: %w", err) + } + + // Otherwise, we'll just set the combined nonce directly. + s.combinedNonce = &combinedNonce + + return nil +} + // Sign generates a partial signature for the target message, using the target // context. If this method is called more than once per context, then an error // is returned, as that means a nonce was re-used. diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index dfd48f3e82..c2efe4afcf 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -439,3 +439,326 @@ func (mr *memsetRandReader) Read(buf []byte) (n int, err error) { } return len(buf), nil } + +// TestSigningWithAggregatedNonce tests the aggregated nonce signing flow where +// nonces are aggregated externally and provided to participants via +// RegisterCombinedNonce, rather than each participant aggregating nonces +// themselves via RegisterPubNonce. +func TestSigningWithAggregatedNonce(t *testing.T) { + t.Run("basic flow", func(t *testing.T) { + const numSigners = 5 + + // Generate signers. + signerKeys := make([]*btcec.PrivateKey, numSigners) + signSet := make([]*btcec.PublicKey, numSigners) + for i := 0; i < numSigners; i++ { + privKey, err := btcec.NewPrivateKey() + if err != nil { + t.Fatalf("unable to gen priv key: %v", err) + } + signerKeys[i] = privKey + signSet[i] = privKey.PubKey() + } + + // Each signer creates a context and session. + sessions := make([]*Session, numSigners) + for i, signerKey := range signerKeys { + signCtx, err := NewContext( + signerKey, false, WithKnownSigners(signSet), + ) + if err != nil { + t.Fatalf("unable to generate context: %v", err) + } + + session, err := signCtx.NewSession() + if err != nil { + t.Fatalf("unable to generate new session: %v", err) + } + sessions[i] = session + } + + // Phase 1: Collect all public nonces. + pubNonces := make([][PubNonceSize]byte, numSigners) + for i, session := range sessions { + pubNonces[i] = session.PublicNonce() + } + + // Phase 2: Aggregate nonces externally. + combinedNonce, err := AggregateNonces(pubNonces) + if err != nil { + t.Fatalf("unable to aggregate nonces: %v", err) + } + + // Phase 3: Participants register combined nonce and sign. + msg := sha256.Sum256([]byte("aggregated nonce signing")) + + partialSigs := make([]*PartialSignature, numSigners) + for i, session := range sessions { + err = session.RegisterCombinedNonce(combinedNonce) + if err != nil { + t.Fatalf("signer %d unable to register combined nonce: %v", + i, err) + } + sig, err := session.Sign(msg) + if err != nil { + t.Fatalf("signer %d unable to sign: %v", i, err) + } + partialSigs[i] = sig + } + + // Phase 4: Combine all partial signatures. + finalSig := CombineSigs(partialSigs[0].R, partialSigs) + + // Verify the final signature. + combinedKey, _, _, err := AggregateKeys(signSet, false) + if err != nil { + t.Fatalf("unable to aggregate keys: %v", err) + } + + if !finalSig.Verify(msg[:], combinedKey.FinalKey) { + t.Fatalf("final signature is invalid") + } + }) + + t.Run("error: register combined nonce twice", func(t *testing.T) { + privKey, _ := btcec.NewPrivateKey() + privKey2, _ := btcec.NewPrivateKey() + signSet := []*btcec.PublicKey{privKey.PubKey(), privKey2.PubKey()} + + signCtx, _ := NewContext(privKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + + fakeCombinedNonce := getValidNonce(t) + + // First call should succeed. + err := session.RegisterCombinedNonce(fakeCombinedNonce) + if err != nil { + t.Fatalf("first RegisterCombinedNonce failed: %v", err) + } + + // Second call should fail. + err = session.RegisterCombinedNonce(fakeCombinedNonce) + if err != ErrAlredyHaveAllNonces { + t.Fatalf("expected ErrAlredyHaveAllNonces, got: %v", err) + } + }) + + t.Run("error: register combined nonce after register pub nonce", + func(t *testing.T) { + + privKey, _ := btcec.NewPrivateKey() + privKey2, _ := btcec.NewPrivateKey() + privKey3, _ := btcec.NewPrivateKey() + signSet := []*btcec.PublicKey{ + privKey.PubKey(), + privKey2.PubKey(), + privKey3.PubKey(), + } + + signCtx, _ := NewContext(privKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + + signCtx2, _ := NewContext(privKey2, false, WithKnownSigners(signSet)) + session2, _ := signCtx2.NewSession() + + // Register one public nonce first. + _, err := session.RegisterPubNonce(session2.PublicNonce()) + if err != nil { + t.Fatalf("RegisterPubNonce failed: %v", err) + } + + // Now try to register a combined nonce - this should fail. + fakeCombinedNonce := [PubNonceSize]byte{} + err = session.RegisterCombinedNonce(fakeCombinedNonce) + if err == nil { + t.Fatalf("expected error when calling RegisterCombinedNonce " + + "after RegisterPubNonce") + } + }) + + t.Run("error: register pub nonce after register combined nonce", + func(t *testing.T) { + + const numSigners = 3 + + signerKeys := make([]*btcec.PrivateKey, numSigners) + signSet := make([]*btcec.PublicKey, numSigners) + for i := 0; i < numSigners; i++ { + privKey, _ := btcec.NewPrivateKey() + signerKeys[i] = privKey + signSet[i] = privKey.PubKey() + } + + sessions := make([]*Session, numSigners) + for i, signerKey := range signerKeys { + signCtx, _ := NewContext(signerKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + sessions[i] = session + } + + pubNonces := make([][PubNonceSize]byte, numSigners) + for i, session := range sessions { + pubNonces[i] = session.PublicNonce() + } + + combinedNonce, _ := AggregateNonces(pubNonces) + + // Register the combined nonce first. + err := sessions[0].RegisterCombinedNonce(combinedNonce) + if err != nil { + t.Fatalf("RegisterCombinedNonce failed: %v", err) + } + + // Now try to register individual nonces - this should fail. + _, err = sessions[0].RegisterPubNonce(pubNonces[1]) + if err == nil { + t.Fatalf("expected error when calling RegisterPubNonce " + + "after RegisterCombinedNonce") + } + }) + + t.Run("nonce reuse prevention", func(t *testing.T) { + privKey, _ := btcec.NewPrivateKey() + privKey2, _ := btcec.NewPrivateKey() + signSet := []*btcec.PublicKey{privKey.PubKey(), privKey2.PubKey()} + + signCtx, _ := NewContext(privKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + + fakeCombinedNonce := getValidNonce(t) + session.RegisterCombinedNonce(fakeCombinedNonce) + + msg := sha256.Sum256([]byte("nonce reuse test")) + + // First sign should succeed. + _, err := session.Sign(msg) + if err != nil { + t.Fatalf("first sign failed: %v", err) + } + + // Second sign should fail due to nonce reuse. + _, err = session.Sign(msg) + if err != ErrSigningContextReuse { + t.Fatalf("expected nonce reuse error, got: %v", err) + } + }) + + t.Run("incorrect combined nonce produces invalid sig", func(t *testing.T) { + const numSigners = 3 + + signerKeys := make([]*btcec.PrivateKey, numSigners) + signSet := make([]*btcec.PublicKey, numSigners) + for i := 0; i < numSigners; i++ { + privKey, _ := btcec.NewPrivateKey() + signerKeys[i] = privKey + signSet[i] = privKey.PubKey() + } + + sessions := make([]*Session, numSigners) + for i, signerKey := range signerKeys { + signCtx, _ := NewContext(signerKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + sessions[i] = session + } + + pubNonces := make([][PubNonceSize]byte, numSigners) + for i, session := range sessions { + pubNonces[i] = session.PublicNonce() + } + + // Create INCORRECT combined nonce using only a subset. + wrongNonces := pubNonces[:2] + incorrectCombinedNonce, _ := AggregateNonces(wrongNonces) + + msg := sha256.Sum256([]byte("incorrect nonce test")) + + partialSigs := make([]*PartialSignature, numSigners) + for i, session := range sessions { + session.RegisterCombinedNonce(incorrectCombinedNonce) + sig, _ := session.Sign(msg) + partialSigs[i] = sig + } + + finalSig := CombineSigs(partialSigs[0].R, partialSigs) + combinedKey, _, _, _ := AggregateKeys(signSet, false) + + // Final signature should be INVALID. + if finalSig.Verify(msg[:], combinedKey.FinalKey) { + t.Fatalf("final signature should be invalid with incorrect nonce") + } + }) + + t.Run("mixed registration methods", func(t *testing.T) { + const numSigners = 4 + + signerKeys := make([]*btcec.PrivateKey, numSigners) + signSet := make([]*btcec.PublicKey, numSigners) + for i := 0; i < numSigners; i++ { + privKey, _ := btcec.NewPrivateKey() + signerKeys[i] = privKey + signSet[i] = privKey.PubKey() + } + + sessions := make([]*Session, numSigners) + for i, signerKey := range signerKeys { + signCtx, _ := NewContext(signerKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + sessions[i] = session + } + + pubNonces := make([][PubNonceSize]byte, numSigners) + for i, session := range sessions { + pubNonces[i] = session.PublicNonce() + } + + combinedNonce, _ := AggregateNonces(pubNonces) + msg := sha256.Sum256([]byte("mixed registration test")) + + // Half use RegisterCombinedNonce. + for i := 0; i < numSigners/2; i++ { + sessions[i].RegisterCombinedNonce(combinedNonce) + } + + // Other half use RegisterPubNonce. + for i := numSigners / 2; i < numSigners; i++ { + for j, nonce := range pubNonces { + if i == j { + continue + } + sessions[i].RegisterPubNonce(nonce) + } + } + + // All should be able to sign. + partialSigs := make([]*PartialSignature, numSigners) + for i, session := range sessions { + sig, err := session.Sign(msg) + if err != nil { + t.Fatalf("signer %d unable to sign: %v", i, err) + } + partialSigs[i] = sig + } + + finalSig := CombineSigs(partialSigs[0].R, partialSigs) + combinedKey, _, _, _ := AggregateKeys(signSet, false) + + if !finalSig.Verify(msg[:], combinedKey.FinalKey) { + t.Fatalf("final signature is invalid") + } + }) +} + +func getValidNonce(t *testing.T) [PubNonceSize]byte { + t.Helper() + + var nonce [PubNonceSize]byte + + privKey, err := btcec.NewPrivateKey() + if err != nil { + t.Fatalf("unable to gen priv key: %v", err) + } + copy(nonce[:33], privKey.PubKey().SerializeCompressed()) + copy(nonce[33:], privKey.PubKey().SerializeCompressed()) + + return nonce +} From 21eb99e3bc0b4b6e0f948578ed6f1997908c5b16 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Wed, 8 Oct 2025 12:15:04 +0200 Subject: [PATCH 2/2] musig2: add combinedNonce getter --- btcec/schnorr/musig2/context.go | 14 +++++ btcec/schnorr/musig2/musig2_test.go | 80 +++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index 54c0a43d68..3effa71e6e 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -553,6 +553,20 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { return haveAllNonces, nil } +// CombinedNonce returns the combined public nonce for the signing session. +// This will be available after either: +// - All individual nonces have been registered via RegisterPubNonce, or +// - A combined nonce has been registered via RegisterCombinedNonce +// +// If the combined nonce is not yet available, this method returns an error. +func (s *Session) CombinedNonce() ([PubNonceSize]byte, error) { + if s.combinedNonce == nil { + return [PubNonceSize]byte{}, ErrCombinedNonceUnavailable + } + + return *s.combinedNonce, nil +} + // RegisterCombinedNonce allows a caller to directly register a combined nonce // that was generated externally. This is useful in coordinator-based // protocols where the coordinator aggregates all nonces and distributes the diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index c2efe4afcf..ebbe055b6d 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -746,6 +746,86 @@ func TestSigningWithAggregatedNonce(t *testing.T) { t.Fatalf("final signature is invalid") } }) + + t.Run("get combined nonce after RegisterCombinedNonce", func(t *testing.T) { + privKey, _ := btcec.NewPrivateKey() + privKey2, _ := btcec.NewPrivateKey() + signSet := []*btcec.PublicKey{privKey.PubKey(), privKey2.PubKey()} + + signCtx, _ := NewContext(privKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + + // Should fail before registering combined nonce. + _, err := session.CombinedNonce() + if err != ErrCombinedNonceUnavailable { + t.Fatalf("expected ErrCombinedNonceUnavailable, got: %v", err) + } + + // Register combined nonce. + expectedNonce := getValidNonce(t) + err = session.RegisterCombinedNonce(expectedNonce) + if err != nil { + t.Fatalf("RegisterCombinedNonce failed: %v", err) + } + + // Should succeed after registering. + gotNonce, err := session.CombinedNonce() + if err != nil { + t.Fatalf("CombinedNonce failed: %v", err) + } + + if gotNonce != expectedNonce { + t.Fatalf("expected nonce %x, got %x", expectedNonce, gotNonce) + } + }) + + t.Run("get combined nonce after RegisterPubNonce", func(t *testing.T) { + const numSigners = 3 + + signerKeys := make([]*btcec.PrivateKey, numSigners) + signSet := make([]*btcec.PublicKey, numSigners) + for i := 0; i < numSigners; i++ { + privKey, _ := btcec.NewPrivateKey() + signerKeys[i] = privKey + signSet[i] = privKey.PubKey() + } + + sessions := make([]*Session, numSigners) + for i, signerKey := range signerKeys { + signCtx, _ := NewContext(signerKey, false, WithKnownSigners(signSet)) + session, _ := signCtx.NewSession() + sessions[i] = session + } + + pubNonces := make([][PubNonceSize]byte, numSigners) + for i, session := range sessions { + pubNonces[i] = session.PublicNonce() + } + + // Should fail before all nonces are registered. + _, err := sessions[0].CombinedNonce() + if err != ErrCombinedNonceUnavailable { + t.Fatalf("expected ErrCombinedNonceUnavailable before all nonces, got: %v", err) + } + + // Register all nonces via RegisterPubNonce. + for i := 1; i < numSigners; i++ { + sessions[0].RegisterPubNonce(pubNonces[i]) + } + + // Should succeed after all nonces are registered. + gotNonce, err := sessions[0].CombinedNonce() + if err != nil { + t.Fatalf("CombinedNonce failed: %v", err) + } + + // Verify it matches what AggregateNonces produces. + expectedNonce, _ := AggregateNonces(pubNonces) + if gotNonce != expectedNonce { + t.Fatalf("combined nonce mismatch: expected %x, got %x", + expectedNonce[:8], gotNonce[:8]) + } + }) } func getValidNonce(t *testing.T) [PubNonceSize]byte {