itest+lntest: add coordinator pattern test for combined nonce

Add integration test for MuSig2RegisterCombinedNonce and
MuSig2GetCombinedNonce RPCs to verify the coordinator pattern workflow.

The test:
- Creates three signing sessions without initial nonce exchange
- Manually aggregates nonces using the coordinator pattern (btcec musig2)
- Tests v0.4.0 returns unsupported errors (as expected)
- Tests v1.0.0rc2 successfully registers and retrieves combined nonces
- Verifies mutual exclusivity (error: already have all nonces)
- Completes a full signing flow to ensure signatures are valid

Also adds the required RPC harness wrapper methods to lntest/rpc/signer.go for
the new RPCs and adds MuSig2RegisterNoncesErr wrapper for error testing.
This commit is contained in:
Elle Mouton committed 2025-12-10 07:19:04 +02:00
1 parent 16b9192dad
commit fb48697ffa
2 files changed
+262

No files matched your search

+188
View File
@@ -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))
}
+74
View File
@@ -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 {