itest+lntest: add coordinator pattern test for combined nonce
What changed, and why it matters
This commit only adds new integration tests and test helper wrappers for the MuSig2 'coordinator pattern' RPCs. It does not change any production code, wallet logic, or consensus behavior. There is no security vulnerability here.
No action required; this is a test-only change. Reviewers may optionally confirm the new test passes in CI and that the harness wrappers correctly propagate errors.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff adds an integration test in itest/lnd_taproot_test.go that exercises MuSig2RegisterCombinedNonce and MuSig2GetCombinedNonce across MuSig2 versions v0.4.0 and v1.0.0rc2, plus supporting harness methods in lntest/rpc/signer.go. No implementation code is modified; the RPCs being tested already exist. The test verifies expected unsupported errors on v0.4.0, successful combined nonce registration/retrieval on v1.0.0rc2, mutual exclusivity with individual nonce registration, and a complete valid signing flow.
Changed components
itest/lnd_taproot_test.golntest/rpc/signer.goInspect captured patch +262 / −0
diff --git a/itest/lnd_taproot_test.go b/itest/lnd_taproot_test.go
index 8421029..9da9ccb 100644
--- a/itest/lnd_taproot_test.go
+++ b/itest/lnd_taproot_test.go
@@ -9,6 +9,7 @@ import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
+ "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -79,6 +80,7 @@ func testTaprootMuSig2(ht *lntest.HarnessTest) {
testTaprootMuSig2ScriptSpend(ht, alice, version)
testTaprootMuSig2CombinedLeafKeySpend(ht, alice, version)
testMuSig2CombineKey(ht, alice, version)
+ testTaprootMuSig2CombinedNonceCoordinator(ht, alice, version)
}
}
@@ -2113,3 +2115,189 @@ func testMuSig2CombineKey(ht *lntest.HarnessTest, alice *node.HarnessNode,
)
}
}
+
+// testTaprootMuSig2CombinedNonceCoordinator tests the coordinator pattern where
+// a single party aggregates all nonces and distributes the combined nonce to
+// participants using MuSig2RegisterCombinedNonce.
+func testTaprootMuSig2CombinedNonceCoordinator(ht *lntest.HarnessTest,
+ alice *node.HarnessNode, version signrpc.MuSig2Version) {
+
+ // We're using a simple BIP-86 key spend only setup.
+ taprootTweak := &signrpc.TaprootTweakDesc{
+ KeySpendOnly: true,
+ }
+
+ // Derive signing keys for our three participants.
+ keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
+ ht, alice, version,
+ )
+
+ // Create three sessions WITHOUT exchanging nonces initially. This
+ // simulates the coordinator pattern where the coordinator collects
+ // nonces first, then aggregates them externally.
+ sessResp1 := alice.RPC.MuSig2CreateSession(
+ &signrpc.MuSig2SessionRequest{
+ KeyLoc: keyDesc1.KeyLoc,
+ AllSignerPubkeys: allPubKeys,
+ TaprootTweak: taprootTweak,
+ Version: version,
+ },
+ )
+ require.Equal(ht, version, sessResp1.Version)
+ require.False(ht, sessResp1.HaveAllNonces)
+
+ sessResp2 := alice.RPC.MuSig2CreateSession(
+ &signrpc.MuSig2SessionRequest{
+ KeyLoc: keyDesc2.KeyLoc,
+ AllSignerPubkeys: allPubKeys,
+ TaprootTweak: taprootTweak,
+ Version: version,
+ },
+ )
+ require.False(ht, sessResp2.HaveAllNonces)
+
+ sessResp3 := alice.RPC.MuSig2CreateSession(
+ &signrpc.MuSig2SessionRequest{
+ KeyLoc: keyDesc3.KeyLoc,
+ AllSignerPubkeys: allPubKeys,
+ TaprootTweak: taprootTweak,
+ Version: version,
+ },
+ )
+ require.False(ht, sessResp3.HaveAllNonces)
+
+ // The coordinator collects all individual nonces.
+ allNonces := [][]byte{
+ sessResp1.LocalPublicNonces,
+ sessResp2.LocalPublicNonces,
+ sessResp3.LocalPublicNonces,
+ }
+
+ // For v0.4.0, both RegisterCombinedNonce and GetCombinedNonce should
+ // return unsupported errors.
+ if version == signrpc.MuSig2Version_MUSIG2_VERSION_V040 {
+ // Try to register a combined nonce - should fail with
+ // unsupported error.
+ var dummyNonce [66]byte
+ err := alice.RPC.MuSig2RegisterCombinedNonceErr(
+ &signrpc.MuSig2RegisterCombinedNonceRequest{
+ SessionId: sessResp1.SessionId,
+ CombinedPublicNonce: dummyNonce[:],
+ },
+ )
+ require.ErrorContains(ht, err, "not supported")
+
+ // Try to get combined nonce - should also fail.
+ err = alice.RPC.MuSig2GetCombinedNonceErr(
+ &signrpc.MuSig2GetCombinedNonceRequest{
+ SessionId: sessResp1.SessionId,
+ },
+ )
+ require.ErrorContains(ht, err, "not supported")
+
+ // For v0.4.0, we can't proceed with the coordinator pattern,
+ // so we're done with this version.
+ return
+ }
+
+ // Copy the nonces over to slice of fixed byte arrays and then use the
+ // musig2 library to aggregate them.
+ var nonces [][musig2.PubNonceSize]byte
+ for _, nonce := range allNonces {
+ var n [musig2.PubNonceSize]byte
+ copy(n[:], nonce)
+ nonces = append(nonces, n)
+ }
+
+ combinedNonce, err := musig2.AggregateNonces(nonces)
+ require.NoError(ht, err)
+
+ // The coordinator now distributes the combined nonce to all
+ // participants.
+ alice.RPC.MuSig2RegisterCombinedNonce(
+ &signrpc.MuSig2RegisterCombinedNonceRequest{
+ SessionId: sessResp1.SessionId,
+ CombinedPublicNonce: combinedNonce[:],
+ },
+ )
+
+ alice.RPC.MuSig2RegisterCombinedNonce(
+ &signrpc.MuSig2RegisterCombinedNonceRequest{
+ SessionId: sessResp2.SessionId,
+ CombinedPublicNonce: combinedNonce[:],
+ },
+ )
+
+ alice.RPC.MuSig2RegisterCombinedNonce(
+ &signrpc.MuSig2RegisterCombinedNonceRequest{
+ SessionId: sessResp3.SessionId,
+ CombinedPublicNonce: combinedNonce[:],
+ },
+ )
+
+ // Verify we can retrieve the combined nonce.
+ getNonceResp := alice.RPC.MuSig2GetCombinedNonce(
+ &signrpc.MuSig2GetCombinedNonceRequest{
+ SessionId: sessResp1.SessionId,
+ },
+ )
+ require.Equal(ht, combinedNonce[:], getNonceResp.CombinedPublicNonce)
+
+ // Test mutual exclusivity: trying to register individual nonces after
+ // combined nonce should fail.
+ err = alice.RPC.MuSig2RegisterNoncesErr(
+ &signrpc.MuSig2RegisterNoncesRequest{
+ SessionId: sessResp1.SessionId,
+ OtherSignerPublicNonces: [][]byte{
+ sessResp2.LocalPublicNonces,
+ },
+ },
+ )
+ require.ErrorContains(ht, err, "already have all nonces")
+
+ // Now complete a full signing flow to verify everything works.
+ combinedKey, err := schnorr.ParsePubKey(sessResp1.CombinedKey)
+ require.NoError(ht, err)
+
+ // Create a simple message to sign.
+ var msg [32]byte
+ copy(msg[:], []byte("test message for combined nonce"))
+
+ // All three participants sign the message.
+ signReq := &signrpc.MuSig2SignRequest{
+ SessionId: sessResp1.SessionId,
+ MessageDigest: msg[:],
+ }
+ alice.RPC.MuSig2Sign(signReq)
+
+ signReq = &signrpc.MuSig2SignRequest{
+ SessionId: sessResp2.SessionId,
+ MessageDigest: msg[:],
+ Cleanup: true,
+ }
+ signResp2 := alice.RPC.MuSig2Sign(signReq)
+
+ signReq = &signrpc.MuSig2SignRequest{
+ SessionId: sessResp3.SessionId,
+ MessageDigest: msg[:],
+ Cleanup: true,
+ }
+ signResp3 := alice.RPC.MuSig2Sign(signReq)
+
+ // Combine the signatures.
+ combineReq := &signrpc.MuSig2CombineSigRequest{
+ SessionId: sessResp1.SessionId,
+ OtherPartialSignatures: [][]byte{
+ signResp2.LocalPartialSignature,
+ signResp3.LocalPartialSignature,
+ },
+ }
+ combineResp := alice.RPC.MuSig2CombineSig(combineReq)
+ require.True(ht, combineResp.HaveAllSignatures)
+ require.NotEmpty(ht, combineResp.FinalSignature)
+
+ // Verify the final signature is valid.
+ sig, err := schnorr.ParseSignature(combineResp.FinalSignature)
+ require.NoError(ht, err)
+ require.True(ht, sig.Verify(msg[:], combinedKey))
+}
diff --git a/lntest/rpc/signer.go b/lntest/rpc/signer.go
index 62b9ac0..5c3c1f6 100644
--- a/lntest/rpc/signer.go
+++ b/lntest/rpc/signer.go
@@ -130,6 +130,80 @@ func (h *HarnessRPC) MuSig2RegisterNonces(
return resp
}
+// MuSig2RegisterNoncesErr makes a RPC call to the node's SignerClient and
+// asserts an error is returned.
+func (h *HarnessRPC) MuSig2RegisterNoncesErr(
+ req *signrpc.MuSig2RegisterNoncesRequest) error {
+
+ ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
+ defer cancel()
+
+ _, err := h.Signer.MuSig2RegisterNonces(ctxt, req)
+ require.Error(h, err, "expected error from MuSig2RegisterNonces")
+
+ return err
+}
+
+// MuSig2RegisterCombinedNonce makes a RPC call to the node's SignerClient and
+// asserts.
+//
+//nolint:ll
+func (h *HarnessRPC) MuSig2RegisterCombinedNonce(
+ req *signrpc.MuSig2RegisterCombinedNonceRequest) *signrpc.MuSig2RegisterCombinedNonceResponse {
+
+ ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
+ defer cancel()
+
+ resp, err := h.Signer.MuSig2RegisterCombinedNonce(ctxt, req)
+ h.NoError(err, "MuSig2RegisterCombinedNonce")
+
+ return resp
+}
+
+// MuSig2RegisterCombinedNonceErr makes a RPC call to the node's SignerClient
+// and asserts an error is returned.
+func (h *HarnessRPC) MuSig2RegisterCombinedNonceErr(
+ req *signrpc.MuSig2RegisterCombinedNonceRequest) error {
+
+ ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
+ defer cancel()
+
+ _, err := h.Signer.MuSig2RegisterCombinedNonce(ctxt, req)
+ require.Error(h, err, "expected error from MuSig2RegisterCombinedNonce")
+
+ return err
+}
+
+// MuSig2GetCombinedNonce makes a RPC call to the node's SignerClient and
+// asserts.
+//
+//nolint:ll
+func (h *HarnessRPC) MuSig2GetCombinedNonce(
+ req *signrpc.MuSig2GetCombinedNonceRequest) *signrpc.MuSig2GetCombinedNonceResponse {
+
+ ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
+ defer cancel()
+
+ resp, err := h.Signer.MuSig2GetCombinedNonce(ctxt, req)
+ h.NoError(err, "MuSig2GetCombinedNonce")
+
+ return resp
+}
+
+// MuSig2GetCombinedNonceErr makes a RPC call to the node's SignerClient and
+// asserts an error is returned.
+func (h *HarnessRPC) MuSig2GetCombinedNonceErr(
+ req *signrpc.MuSig2GetCombinedNonceRequest) error {
+
+ ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
+ defer cancel()
+
+ _, err := h.Signer.MuSig2GetCombinedNonce(ctxt, req)
+ require.Error(h, err, "expected error from MuSig2GetCombinedNonce")
+
+ return err
+}
+
// MuSig2Sign makes a RPC call to the node's SignerClient and asserts.
func (h *HarnessRPC) MuSig2Sign(
req *signrpc.MuSig2SignRequest) *signrpc.MuSig2SignResponse {
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.