|
9 | 9 | "github.com/btcsuite/btcd/blockchain" |
10 | 10 | "github.com/btcsuite/btcd/btcec/v2" |
11 | 11 | "github.com/btcsuite/btcd/btcec/v2/schnorr" |
| 12 | + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" |
12 | 13 | "github.com/btcsuite/btcd/btcutil" |
13 | 14 | "github.com/btcsuite/btcd/btcutil/psbt" |
14 | 15 | "github.com/btcsuite/btcd/chaincfg/chainhash" |
@@ -79,6 +80,7 @@ func testTaprootMuSig2(ht *lntest.HarnessTest) { |
79 | 80 | testTaprootMuSig2ScriptSpend(ht, alice, version) |
80 | 81 | testTaprootMuSig2CombinedLeafKeySpend(ht, alice, version) |
81 | 82 | testMuSig2CombineKey(ht, alice, version) |
| 83 | + testTaprootMuSig2CombinedNonceCoordinator(ht, alice, version) |
82 | 84 | } |
83 | 85 | } |
84 | 86 |
|
@@ -2113,3 +2115,189 @@ func testMuSig2CombineKey(ht *lntest.HarnessTest, alice *node.HarnessNode, |
2113 | 2115 | ) |
2114 | 2116 | } |
2115 | 2117 | } |
| 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 | +} |
0 commit comments