Skip to content

Commit fb48697

Browse files
committed
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.
1 parent 16b9192 commit fb48697

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed

itest/lnd_taproot_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/btcsuite/btcd/blockchain"
1010
"github.com/btcsuite/btcd/btcec/v2"
1111
"github.com/btcsuite/btcd/btcec/v2/schnorr"
12+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
1213
"github.com/btcsuite/btcd/btcutil"
1314
"github.com/btcsuite/btcd/btcutil/psbt"
1415
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -79,6 +80,7 @@ func testTaprootMuSig2(ht *lntest.HarnessTest) {
7980
testTaprootMuSig2ScriptSpend(ht, alice, version)
8081
testTaprootMuSig2CombinedLeafKeySpend(ht, alice, version)
8182
testMuSig2CombineKey(ht, alice, version)
83+
testTaprootMuSig2CombinedNonceCoordinator(ht, alice, version)
8284
}
8385
}
8486

@@ -2113,3 +2115,189 @@ func testMuSig2CombineKey(ht *lntest.HarnessTest, alice *node.HarnessNode,
21132115
)
21142116
}
21152117
}
2118+
2119+
// testTaprootMuSig2CombinedNonceCoordinator tests the coordinator pattern where
2120+
// a single party aggregates all nonces and distributes the combined nonce to
2121+
// participants using MuSig2RegisterCombinedNonce.
2122+
func testTaprootMuSig2CombinedNonceCoordinator(ht *lntest.HarnessTest,
2123+
alice *node.HarnessNode, version signrpc.MuSig2Version) {
2124+
2125+
// We're using a simple BIP-86 key spend only setup.
2126+
taprootTweak := &signrpc.TaprootTweakDesc{
2127+
KeySpendOnly: true,
2128+
}
2129+
2130+
// Derive signing keys for our three participants.
2131+
keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
2132+
ht, alice, version,
2133+
)
2134+
2135+
// Create three sessions WITHOUT exchanging nonces initially. This
2136+
// simulates the coordinator pattern where the coordinator collects
2137+
// nonces first, then aggregates them externally.
2138+
sessResp1 := alice.RPC.MuSig2CreateSession(
2139+
&signrpc.MuSig2SessionRequest{
2140+
KeyLoc: keyDesc1.KeyLoc,
2141+
AllSignerPubkeys: allPubKeys,
2142+
TaprootTweak: taprootTweak,
2143+
Version: version,
2144+
},
2145+
)
2146+
require.Equal(ht, version, sessResp1.Version)
2147+
require.False(ht, sessResp1.HaveAllNonces)
2148+
2149+
sessResp2 := alice.RPC.MuSig2CreateSession(
2150+
&signrpc.MuSig2SessionRequest{
2151+
KeyLoc: keyDesc2.KeyLoc,
2152+
AllSignerPubkeys: allPubKeys,
2153+
TaprootTweak: taprootTweak,
2154+
Version: version,
2155+
},
2156+
)
2157+
require.False(ht, sessResp2.HaveAllNonces)
2158+
2159+
sessResp3 := alice.RPC.MuSig2CreateSession(
2160+
&signrpc.MuSig2SessionRequest{
2161+
KeyLoc: keyDesc3.KeyLoc,
2162+
AllSignerPubkeys: allPubKeys,
2163+
TaprootTweak: taprootTweak,
2164+
Version: version,
2165+
},
2166+
)
2167+
require.False(ht, sessResp3.HaveAllNonces)
2168+
2169+
// The coordinator collects all individual nonces.
2170+
allNonces := [][]byte{
2171+
sessResp1.LocalPublicNonces,
2172+
sessResp2.LocalPublicNonces,
2173+
sessResp3.LocalPublicNonces,
2174+
}
2175+
2176+
// For v0.4.0, both RegisterCombinedNonce and GetCombinedNonce should
2177+
// return unsupported errors.
2178+
if version == signrpc.MuSig2Version_MUSIG2_VERSION_V040 {
2179+
// Try to register a combined nonce - should fail with
2180+
// unsupported error.
2181+
var dummyNonce [66]byte
2182+
err := alice.RPC.MuSig2RegisterCombinedNonceErr(
2183+
&signrpc.MuSig2RegisterCombinedNonceRequest{
2184+
SessionId: sessResp1.SessionId,
2185+
CombinedPublicNonce: dummyNonce[:],
2186+
},
2187+
)
2188+
require.ErrorContains(ht, err, "not supported")
2189+
2190+
// Try to get combined nonce - should also fail.
2191+
err = alice.RPC.MuSig2GetCombinedNonceErr(
2192+
&signrpc.MuSig2GetCombinedNonceRequest{
2193+
SessionId: sessResp1.SessionId,
2194+
},
2195+
)
2196+
require.ErrorContains(ht, err, "not supported")
2197+
2198+
// For v0.4.0, we can't proceed with the coordinator pattern,
2199+
// so we're done with this version.
2200+
return
2201+
}
2202+
2203+
// Copy the nonces over to slice of fixed byte arrays and then use the
2204+
// musig2 library to aggregate them.
2205+
var nonces [][musig2.PubNonceSize]byte
2206+
for _, nonce := range allNonces {
2207+
var n [musig2.PubNonceSize]byte
2208+
copy(n[:], nonce)
2209+
nonces = append(nonces, n)
2210+
}
2211+
2212+
combinedNonce, err := musig2.AggregateNonces(nonces)
2213+
require.NoError(ht, err)
2214+
2215+
// The coordinator now distributes the combined nonce to all
2216+
// participants.
2217+
alice.RPC.MuSig2RegisterCombinedNonce(
2218+
&signrpc.MuSig2RegisterCombinedNonceRequest{
2219+
SessionId: sessResp1.SessionId,
2220+
CombinedPublicNonce: combinedNonce[:],
2221+
},
2222+
)
2223+
2224+
alice.RPC.MuSig2RegisterCombinedNonce(
2225+
&signrpc.MuSig2RegisterCombinedNonceRequest{
2226+
SessionId: sessResp2.SessionId,
2227+
CombinedPublicNonce: combinedNonce[:],
2228+
},
2229+
)
2230+
2231+
alice.RPC.MuSig2RegisterCombinedNonce(
2232+
&signrpc.MuSig2RegisterCombinedNonceRequest{
2233+
SessionId: sessResp3.SessionId,
2234+
CombinedPublicNonce: combinedNonce[:],
2235+
},
2236+
)
2237+
2238+
// Verify we can retrieve the combined nonce.
2239+
getNonceResp := alice.RPC.MuSig2GetCombinedNonce(
2240+
&signrpc.MuSig2GetCombinedNonceRequest{
2241+
SessionId: sessResp1.SessionId,
2242+
},
2243+
)
2244+
require.Equal(ht, combinedNonce[:], getNonceResp.CombinedPublicNonce)
2245+
2246+
// Test mutual exclusivity: trying to register individual nonces after
2247+
// combined nonce should fail.
2248+
err = alice.RPC.MuSig2RegisterNoncesErr(
2249+
&signrpc.MuSig2RegisterNoncesRequest{
2250+
SessionId: sessResp1.SessionId,
2251+
OtherSignerPublicNonces: [][]byte{
2252+
sessResp2.LocalPublicNonces,
2253+
},
2254+
},
2255+
)
2256+
require.ErrorContains(ht, err, "already have all nonces")
2257+
2258+
// Now complete a full signing flow to verify everything works.
2259+
combinedKey, err := schnorr.ParsePubKey(sessResp1.CombinedKey)
2260+
require.NoError(ht, err)
2261+
2262+
// Create a simple message to sign.
2263+
var msg [32]byte
2264+
copy(msg[:], []byte("test message for combined nonce"))
2265+
2266+
// All three participants sign the message.
2267+
signReq := &signrpc.MuSig2SignRequest{
2268+
SessionId: sessResp1.SessionId,
2269+
MessageDigest: msg[:],
2270+
}
2271+
alice.RPC.MuSig2Sign(signReq)
2272+
2273+
signReq = &signrpc.MuSig2SignRequest{
2274+
SessionId: sessResp2.SessionId,
2275+
MessageDigest: msg[:],
2276+
Cleanup: true,
2277+
}
2278+
signResp2 := alice.RPC.MuSig2Sign(signReq)
2279+
2280+
signReq = &signrpc.MuSig2SignRequest{
2281+
SessionId: sessResp3.SessionId,
2282+
MessageDigest: msg[:],
2283+
Cleanup: true,
2284+
}
2285+
signResp3 := alice.RPC.MuSig2Sign(signReq)
2286+
2287+
// Combine the signatures.
2288+
combineReq := &signrpc.MuSig2CombineSigRequest{
2289+
SessionId: sessResp1.SessionId,
2290+
OtherPartialSignatures: [][]byte{
2291+
signResp2.LocalPartialSignature,
2292+
signResp3.LocalPartialSignature,
2293+
},
2294+
}
2295+
combineResp := alice.RPC.MuSig2CombineSig(combineReq)
2296+
require.True(ht, combineResp.HaveAllSignatures)
2297+
require.NotEmpty(ht, combineResp.FinalSignature)
2298+
2299+
// Verify the final signature is valid.
2300+
sig, err := schnorr.ParseSignature(combineResp.FinalSignature)
2301+
require.NoError(ht, err)
2302+
require.True(ht, sig.Verify(msg[:], combinedKey))
2303+
}

lntest/rpc/signer.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,80 @@ func (h *HarnessRPC) MuSig2RegisterNonces(
130130
return resp
131131
}
132132

133+
// MuSig2RegisterNoncesErr makes a RPC call to the node's SignerClient and
134+
// asserts an error is returned.
135+
func (h *HarnessRPC) MuSig2RegisterNoncesErr(
136+
req *signrpc.MuSig2RegisterNoncesRequest) error {
137+
138+
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
139+
defer cancel()
140+
141+
_, err := h.Signer.MuSig2RegisterNonces(ctxt, req)
142+
require.Error(h, err, "expected error from MuSig2RegisterNonces")
143+
144+
return err
145+
}
146+
147+
// MuSig2RegisterCombinedNonce makes a RPC call to the node's SignerClient and
148+
// asserts.
149+
//
150+
//nolint:ll
151+
func (h *HarnessRPC) MuSig2RegisterCombinedNonce(
152+
req *signrpc.MuSig2RegisterCombinedNonceRequest) *signrpc.MuSig2RegisterCombinedNonceResponse {
153+
154+
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
155+
defer cancel()
156+
157+
resp, err := h.Signer.MuSig2RegisterCombinedNonce(ctxt, req)
158+
h.NoError(err, "MuSig2RegisterCombinedNonce")
159+
160+
return resp
161+
}
162+
163+
// MuSig2RegisterCombinedNonceErr makes a RPC call to the node's SignerClient
164+
// and asserts an error is returned.
165+
func (h *HarnessRPC) MuSig2RegisterCombinedNonceErr(
166+
req *signrpc.MuSig2RegisterCombinedNonceRequest) error {
167+
168+
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
169+
defer cancel()
170+
171+
_, err := h.Signer.MuSig2RegisterCombinedNonce(ctxt, req)
172+
require.Error(h, err, "expected error from MuSig2RegisterCombinedNonce")
173+
174+
return err
175+
}
176+
177+
// MuSig2GetCombinedNonce makes a RPC call to the node's SignerClient and
178+
// asserts.
179+
//
180+
//nolint:ll
181+
func (h *HarnessRPC) MuSig2GetCombinedNonce(
182+
req *signrpc.MuSig2GetCombinedNonceRequest) *signrpc.MuSig2GetCombinedNonceResponse {
183+
184+
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
185+
defer cancel()
186+
187+
resp, err := h.Signer.MuSig2GetCombinedNonce(ctxt, req)
188+
h.NoError(err, "MuSig2GetCombinedNonce")
189+
190+
return resp
191+
}
192+
193+
// MuSig2GetCombinedNonceErr makes a RPC call to the node's SignerClient and
194+
// asserts an error is returned.
195+
func (h *HarnessRPC) MuSig2GetCombinedNonceErr(
196+
req *signrpc.MuSig2GetCombinedNonceRequest) error {
197+
198+
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
199+
defer cancel()
200+
201+
_, err := h.Signer.MuSig2GetCombinedNonce(ctxt, req)
202+
require.Error(h, err, "expected error from MuSig2GetCombinedNonce")
203+
204+
return err
205+
}
206+
133207
// MuSig2Sign makes a RPC call to the node's SignerClient and asserts.
134208
func (h *HarnessRPC) MuSig2Sign(
135209
req *signrpc.MuSig2SignRequest) *signrpc.MuSig2SignResponse {

0 commit comments

Comments
 (0)