What changed, and why it matters
This commit adds a new public getter method called CombinedNonce to the MuSig2 signing session in btcd. It simply lets callers read the already-computed combined public nonce, returning an error if it isn't ready yet. There is no change to cryptographic logic, no bug fix, and no security-relevant behavior.
No security action needed. This is a routine API addition with no security implications.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces Session.CombinedNonce() in btcec/schnorr/musig2/context.go. It returns s.combinedNonce if set, otherwise ErrCombinedNonceUnavailable. The combined nonce is already populated by RegisterPubNonce once all nonces are received, or directly by RegisterCombinedNonce. Tests verify the getter returns the expected value in both scenarios and errors before the nonce is available. No state mutation, validation, or algorithmic changes are made.
Changed components
btcec/schnorr/musig2/context.gobtcec/schnorr/musig2/musig2_test.goInspect captured patch +94 / −0
diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go
index 54c0a43..3effa71 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 c2efe4a..ebbe055 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 {
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.