1
+ package fr.acinq.bitcoin.crypto.musig2
2
+
3
+ import fr.acinq.bitcoin.*
4
+ import fr.acinq.bitcoin.utils.Either
5
+ import fr.acinq.bitcoin.utils.flatMap
6
+ import fr.acinq.secp256k1.Hex
7
+ import fr.acinq.secp256k1.Secp256k1
8
+ import kotlin.jvm.JvmOverloads
9
+ import kotlin.jvm.JvmStatic
10
+
11
+ /* *
12
+ * Musig2 key aggregation cache: keeps track of an aggregate of public keys, that can optionally be tweaked.
13
+ * This should be treated as an opaque blob of data, that doesn't contain any sensitive data and thus can be stored.
14
+ */
15
+ public data class KeyAggCache (private val data : ByteVector ) {
16
+ public constructor (data: ByteArray ) : this (data.byteVector())
17
+
18
+ init {
19
+ require(data.size() == Secp256k1 .MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE ) { " musig2 keyagg cache must be ${Secp256k1 .MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE } bytes" }
20
+ }
21
+
22
+ public fun toByteArray (): ByteArray = data.toByteArray()
23
+
24
+ override fun toString (): String = data.toHex()
25
+
26
+ /* *
27
+ * @param tweak tweak to apply.
28
+ * @param isXonly true if the tweak is an x-only tweak.
29
+ * @return an updated cache and the tweaked aggregated public key, or null if one of the tweaks is invalid.
30
+ */
31
+ public fun tweak (tweak : ByteVector32 , isXonly : Boolean ): Either <Throwable , Pair <KeyAggCache , PublicKey >> = try {
32
+ val localCache = toByteArray()
33
+ val tweaked = if (isXonly) {
34
+ Secp256k1 .musigPubkeyXonlyTweakAdd(localCache, tweak.toByteArray())
35
+ } else {
36
+ Secp256k1 .musigPubkeyTweakAdd(localCache, tweak.toByteArray())
37
+ }
38
+ Either .Right (Pair (KeyAggCache (localCache), PublicKey .parse(tweaked)))
39
+ } catch (t: Throwable ) {
40
+ Either .Left (t)
41
+ }
42
+
43
+ public companion object {
44
+ /* *
45
+ * @param publicKeys public keys to aggregate: callers must verify that all public keys are valid.
46
+ * @return an opaque key aggregation cache and the aggregated public key.
47
+ */
48
+ @JvmStatic
49
+ public fun create (publicKeys : List <PublicKey >): Pair <XonlyPublicKey , KeyAggCache > {
50
+ require(publicKeys.all { it.isValid() }) { " some of the public keys provided are not valid" }
51
+ val localCache = ByteArray (Secp256k1 .MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE )
52
+ val aggkey = Secp256k1 .musigPubkeyAgg(publicKeys.map { it.value.toByteArray() }.toTypedArray(), localCache)
53
+ return Pair (XonlyPublicKey (aggkey.byteVector32()), KeyAggCache (localCache.byteVector()))
54
+ }
55
+ }
56
+ }
57
+
58
+ /* *
59
+ * Musig2 signing session context that can be used to create partial signatures and aggregate them.
60
+ */
61
+ public data class Session (private val data : ByteVector , private val keyAggCache : KeyAggCache ) {
62
+ init {
63
+ require(data.size() == Secp256k1 .MUSIG2_PUBLIC_SESSION_SIZE ) { " musig2 session must be ${Secp256k1 .MUSIG2_PUBLIC_SESSION_SIZE } bytes" }
64
+ }
65
+
66
+ public fun toByteArray (): ByteArray = data.toByteArray()
67
+
68
+ /* *
69
+ * @param secretNonce signer's secret nonce (see [SecretNonce.generate]).
70
+ * @param privateKey signer's private key.
71
+ * @return a musig2 partial signature.
72
+ */
73
+ public fun sign (secretNonce : SecretNonce , privateKey : PrivateKey ): ByteVector32 {
74
+ return Secp256k1 .musigPartialSign(secretNonce.data.toByteArray(), privateKey.value.toByteArray(), keyAggCache.toByteArray(), this .toByteArray()).byteVector32()
75
+ }
76
+
77
+ /* *
78
+ * @param partialSig musig2 partial signature.
79
+ * @param publicNonce individual public nonce of the signing participant.
80
+ * @param publicKey individual public key of the signing participant.
81
+ * @return true if the partial signature is valid.
82
+ */
83
+ public fun verify (partialSig : ByteVector32 , publicNonce : IndividualNonce , publicKey : PublicKey ): Boolean = try {
84
+ Secp256k1 .musigPartialSigVerify(partialSig.toByteArray(), publicNonce.toByteArray(), publicKey.value.toByteArray(), keyAggCache.toByteArray(), this .toByteArray()) == 1
85
+ } catch (t: Throwable ) {
86
+ false
87
+ }
88
+
89
+ /* *
90
+ * Aggregate partial signatures from all participants into a single schnorr signature. Callers should verify the
91
+ * resulting signature, which may be invalid without raising an error here (for example if the set of partial
92
+ * signatures is valid but incomplete).
93
+ *
94
+ * @param partialSigs partial signatures from all signing participants.
95
+ * @return the aggregate signature of all input partial signatures or null if a partial signature is invalid.
96
+ */
97
+ public fun aggregateSigs (partialSigs : List <ByteVector32 >): Either <Throwable , ByteVector64 > = try {
98
+ Either .Right (Secp256k1 .musigPartialSigAgg(this .toByteArray(), partialSigs.map { it.toByteArray() }.toTypedArray()).byteVector64())
99
+ } catch (t: Throwable ) {
100
+ Either .Left (t)
101
+ }
102
+
103
+ public companion object {
104
+ /* *
105
+ * @param aggregatedNonce aggregated public nonce.
106
+ * @param message message that will be signed.
107
+ * @param keyAggCache key aggregation cache.
108
+ * @return a musig2 signing session.
109
+ */
110
+ @JvmStatic
111
+ public fun create (aggregatedNonce : AggregatedNonce , message : ByteVector32 , keyAggCache : KeyAggCache ): Session {
112
+ val session = Secp256k1 .musigNonceProcess(aggregatedNonce.toByteArray(), message.toByteArray(), keyAggCache.toByteArray())
113
+ return Session (session.byteVector(), keyAggCache)
114
+ }
115
+ }
116
+ }
117
+
118
+ /* *
119
+ * Musig2 secret nonce, that should be treated as a private opaque blob.
120
+ * This nonce must never be persisted or reused across signing sessions.
121
+ */
122
+ public data class SecretNonce (internal val data : ByteVector ) {
123
+ public constructor (bin: ByteArray ) : this (bin.byteVector())
124
+ public constructor (hex: String ) : this (Hex .decode(hex))
125
+
126
+ init {
127
+ require(data.size() == Secp256k1 .MUSIG2_SECRET_NONCE_SIZE ) { " musig2 secret nonce must be ${Secp256k1 .MUSIG2_SECRET_NONCE_SIZE } bytes" }
128
+ }
129
+
130
+ override fun toString (): String = " <secret_nonce>"
131
+
132
+ public companion object {
133
+ /* *
134
+ * Generate a secret nonce to be used in a musig2 signing session.
135
+ * This nonce must never be persisted or reused across signing sessions.
136
+ * All optional arguments exist to enrich the quality of the randomness used, which is critical for security.
137
+ *
138
+ * @param sessionId unique session ID.
139
+ * @param privateKey (optional) signer's private key.
140
+ * @param publicKey signer's public key.
141
+ * @param message (optional) message that will be signed, if already known.
142
+ * @param keyAggCache (optional) key aggregation cache data from the signing session.
143
+ * @param extraInput (optional) additional random data.
144
+ * @return secret nonce and the corresponding public nonce.
145
+ */
146
+ @JvmStatic
147
+ public fun generate (sessionId : ByteVector32 , privateKey : PrivateKey ? , publicKey : PublicKey , message : ByteVector32 ? , keyAggCache : KeyAggCache ? , extraInput : ByteVector32 ? ): Pair <SecretNonce , IndividualNonce > {
148
+ privateKey?.let { require(it.publicKey() == publicKey) { " if the private key is provided, it must match the public key" } }
149
+ val nonce = Secp256k1 .musigNonceGen(sessionId.toByteArray(), privateKey?.value?.toByteArray(), publicKey.value.toByteArray(), message?.toByteArray(), keyAggCache?.toByteArray(), extraInput?.toByteArray())
150
+ val secretNonce = SecretNonce (nonce.copyOfRange(0 , Secp256k1 .MUSIG2_SECRET_NONCE_SIZE ))
151
+ val publicNonce = IndividualNonce (nonce.copyOfRange(Secp256k1 .MUSIG2_SECRET_NONCE_SIZE , Secp256k1 .MUSIG2_SECRET_NONCE_SIZE + Secp256k1 .MUSIG2_PUBLIC_NONCE_SIZE ))
152
+ return Pair (secretNonce, publicNonce)
153
+ }
154
+ }
155
+ }
156
+
157
+ /* *
158
+ * Musig2 public nonce, that must be shared with other participants in the signing session.
159
+ * It contains two elliptic curve points, but should be treated as an opaque blob.
160
+ */
161
+ public data class IndividualNonce (val data : ByteVector ) {
162
+ public constructor (bin: ByteArray ) : this (bin.byteVector())
163
+ public constructor (hex: String ) : this (Hex .decode(hex))
164
+
165
+ init {
166
+ require(data.size() == Secp256k1 .MUSIG2_PUBLIC_NONCE_SIZE ) { " individual musig2 public nonce must be ${Secp256k1 .MUSIG2_PUBLIC_NONCE_SIZE } bytes" }
167
+ }
168
+
169
+ public fun toByteArray (): ByteArray = data.toByteArray()
170
+
171
+ override fun toString (): String = data.toHex()
172
+
173
+ public companion object {
174
+ /* *
175
+ * Aggregate public nonces from all participants of a signing session.
176
+ * Returns null if one of the nonces provided is invalid.
177
+ */
178
+ @JvmStatic
179
+ public fun aggregate (nonces : List <IndividualNonce >): Either <Throwable , AggregatedNonce > = try {
180
+ val agg = Secp256k1 .musigNonceAgg(nonces.map { it.toByteArray() }.toTypedArray())
181
+ Either .Right (AggregatedNonce (agg))
182
+ } catch (t: Throwable ) {
183
+ Either .Left (t)
184
+ }
185
+ }
186
+ }
187
+
188
+ /* *
189
+ * Musig2 aggregate public nonce from all participants of a signing session.
190
+ */
191
+ public data class AggregatedNonce (val data : ByteVector ) {
192
+ public constructor (bin: ByteArray ) : this (bin.byteVector())
193
+ public constructor (hex: String ) : this (Hex .decode(hex))
194
+
195
+ init {
196
+ require(data.size() == Secp256k1 .MUSIG2_PUBLIC_NONCE_SIZE ) { " aggregated musig2 public nonce must be ${Secp256k1 .MUSIG2_PUBLIC_NONCE_SIZE } bytes" }
197
+ }
198
+
199
+ public fun toByteArray (): ByteArray = data.toByteArray()
200
+
201
+ override fun toString (): String = data.toHex()
202
+ }
203
+
204
+ /* *
205
+ * This object contain helper functions to use musig2 in the context of spending taproot outputs.
206
+ * In order to provide a simpler API, some operations are internally duplicated: if performance is an issue, you should
207
+ * consider using the lower-level APIs directly (see [Session] and [KeyAggCache]).
208
+ */
209
+ public object Musig2 {
210
+ /* *
211
+ * Aggregate the public keys of a musig2 session into a single public key.
212
+ * Note that this function doesn't apply any tweak: when used for taproot, it computes the internal public key, not
213
+ * the public key exposed in the script (which is tweaked with the script tree).
214
+ *
215
+ * @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
216
+ */
217
+ @JvmStatic
218
+ public fun aggregateKeys (publicKeys : List <PublicKey >): XonlyPublicKey = KeyAggCache .create(publicKeys).first
219
+
220
+ /* *
221
+ * @param sessionId a random, unique session ID.
222
+ * @param privateKey signer's private key.
223
+ * @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
224
+ */
225
+ @JvmStatic
226
+ public fun generateNonce (sessionId : ByteVector32 , privateKey : PrivateKey , publicKeys : List <PublicKey >): Pair <SecretNonce , IndividualNonce > {
227
+ val (_, keyAggCache) = KeyAggCache .create(publicKeys)
228
+ return SecretNonce .generate(sessionId, privateKey, privateKey.publicKey(), message = null , keyAggCache, extraInput = null )
229
+ }
230
+
231
+ private fun taprootSession (tx : Transaction , inputIndex : Int , inputs : List <TxOut >, publicKeys : List <PublicKey >, publicNonces : List <IndividualNonce >, scriptTree : ScriptTree ? ): Either <Throwable , Session > {
232
+ return IndividualNonce .aggregate(publicNonces).flatMap { aggregateNonce ->
233
+ val (aggregatePublicKey, keyAggCache) = KeyAggCache .create(publicKeys)
234
+ val tweak = when (scriptTree) {
235
+ null -> aggregatePublicKey.tweak(Crypto .TaprootTweak .NoScriptTweak )
236
+ else -> aggregatePublicKey.tweak(Crypto .TaprootTweak .ScriptTweak (scriptTree))
237
+ }
238
+ keyAggCache.tweak(tweak, isXonly = true ).map { tweakedKeyAggCache ->
239
+ val txHash = Transaction .hashForSigningTaprootKeyPath(tx, inputIndex, inputs, SigHash .SIGHASH_DEFAULT )
240
+ Session .create(aggregateNonce, txHash, tweakedKeyAggCache.first)
241
+ }
242
+ }
243
+ }
244
+
245
+ /* *
246
+ * Create a partial musig2 signature for the given taproot input key path.
247
+ *
248
+ * @param privateKey private key of the signing participant.
249
+ * @param tx transaction spending the target taproot input.
250
+ * @param inputIndex index of the taproot input to spend.
251
+ * @param inputs all inputs of the spending transaction.
252
+ * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid.
253
+ * @param secretNonce secret nonce of the signing participant.
254
+ * @param publicNonces public nonces of all participants of the musig2 session.
255
+ * @param scriptTree tapscript tree of the taproot input, if it has script paths.
256
+ */
257
+ @JvmStatic
258
+ public fun signTaprootInput (
259
+ privateKey : PrivateKey ,
260
+ tx : Transaction ,
261
+ inputIndex : Int ,
262
+ inputs : List <TxOut >,
263
+ publicKeys : List <PublicKey >,
264
+ secretNonce : SecretNonce ,
265
+ publicNonces : List <IndividualNonce >,
266
+ scriptTree : ScriptTree ?
267
+ ): Either <Throwable , ByteVector32 > {
268
+ return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).map { it.sign(secretNonce, privateKey) }
269
+ }
270
+
271
+ /* *
272
+ * Aggregate partial musig2 signatures into a valid schnorr signature for the given taproot input key path.
273
+ *
274
+ * @param partialSigs partial musig2 signatures of all participants of the musig2 session.
275
+ * @param tx transaction spending the target taproot input.
276
+ * @param inputIndex index of the taproot input to spend.
277
+ * @param inputs all inputs of the spending transaction.
278
+ * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid.
279
+ * @param publicNonces public nonces of all participants of the musig2 session.
280
+ * @param scriptTree tapscript tree of the taproot input, if it has script paths.
281
+ */
282
+ @JvmStatic
283
+ public fun aggregateTaprootSignatures (
284
+ partialSigs : List <ByteVector32 >,
285
+ tx : Transaction ,
286
+ inputIndex : Int ,
287
+ inputs : List <TxOut >,
288
+ publicKeys : List <PublicKey >,
289
+ publicNonces : List <IndividualNonce >,
290
+ scriptTree : ScriptTree ?
291
+ ): Either <Throwable , ByteVector64 > {
292
+ return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).flatMap { it.aggregateSigs(partialSigs) }
293
+ }
294
+
295
+ }
0 commit comments