Skip to content

Commit 3fea18e

Browse files
committed
Add wrapper for secp256k1-kmp's musig2 methods
1 parent 4483f07 commit 3fea18e

File tree

3 files changed

+461
-2
lines changed

3 files changed

+461
-2
lines changed

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ plugins {
1212
val currentOs = org.gradle.internal.os.OperatingSystem.current()
1313

1414
group = "fr.acinq.bitcoin"
15-
version = "0.16.0-SNAPSHOT"
15+
version = "0.16.0-MUSIG2-SNAPSHOT"
1616

1717
repositories {
1818
google()
@@ -45,7 +45,7 @@ kotlin {
4545
}
4646

4747
sourceSets {
48-
val secp256k1KmpVersion = "0.13.0-SNAPSHOT"
48+
val secp256k1KmpVersion = "0.13.0-MUSIG2-SNAPSHOT"
4949

5050
val commonMain by getting {
5151
dependencies {
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package fr.acinq.bitcoin.crypto.musig2
2+
3+
import fr.acinq.bitcoin.*
4+
import fr.acinq.secp256k1.Hex
5+
import fr.acinq.secp256k1.Secp256k1
6+
import kotlin.jvm.JvmStatic
7+
8+
/**
9+
* Musig2 key aggregation cache
10+
* Keeps track of an aggregate of public keys, that can optionally be tweaked
11+
*/
12+
public data class KeyAggCache(val data: ByteVector) {
13+
public constructor(data: ByteArray) : this(data.byteVector())
14+
15+
init {
16+
require(data.size() == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) { "musig2 keyagg cache must be ${Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE} bytes" }
17+
}
18+
19+
public fun toByteArray(): ByteArray = data.toByteArray()
20+
21+
/**
22+
* @param tweak tweak to apply
23+
* @param isXonly true if the tweak is an x-only tweak
24+
* @return an updated cache, and the tweaked aggregated public key
25+
*/
26+
public fun tweak(tweak: ByteVector32, isXonly: Boolean): Pair<KeyAggCache, PublicKey> {
27+
val localCache = toByteArray()
28+
val tweaked = if (isXonly) {
29+
Secp256k1.musigPubkeyXonlyTweakAdd(localCache, tweak.toByteArray())
30+
} else {
31+
Secp256k1.musigPubkeyTweakAdd(localCache, tweak.toByteArray())
32+
}
33+
return Pair(KeyAggCache(localCache), PublicKey.parse(tweaked))
34+
}
35+
36+
public companion object {
37+
/**
38+
* @param pubkeys public keys to aggregate
39+
* @param cache an optional key aggregation cache
40+
* @return a new (if cache was null) or updated cache, and the aggregated public key
41+
*/
42+
@JvmStatic
43+
public fun add(pubkeys: List<PublicKey>, cache: KeyAggCache?): Pair<XonlyPublicKey, KeyAggCache> {
44+
val localCache = cache?.data?.toByteArray() ?: ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE)
45+
val aggkey = Secp256k1.musigPubkeyAdd(pubkeys.map { it.value.toByteArray() }.toTypedArray(), localCache)
46+
return Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector()))
47+
}
48+
}
49+
}
50+
51+
/**
52+
* Musig2 signing session
53+
*/
54+
public data class Session(val data: ByteVector) {
55+
init {
56+
require(data.size() == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) { "musig2 session must be ${Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE} bytes" }
57+
}
58+
59+
public fun toByteArray(): ByteArray = data.toByteArray()
60+
61+
/**
62+
* @param secretNonce secret nonce
63+
* @param pk private key
64+
* @param aggCache key aggregation cache
65+
* @return a Musig2 partial signature
66+
*/
67+
public fun sign(secretNonce: SecretNonce, pk: PrivateKey, aggCache: KeyAggCache): ByteVector32 {
68+
return Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), pk.value.toByteArray(), aggCache.data.toByteArray(), toByteArray()).byteVector32()
69+
}
70+
71+
/**
72+
* @param psig musig2 partial signature
73+
* @param pubnonce public nonce, that must match the secret nonce psig was generated with
74+
* @param pubkey public key, that must match the private key psig was generated with
75+
* @param cache key aggregation cache
76+
* @return true if the partial signature is valid
77+
*/
78+
public fun verify(psig: ByteVector32, pubnonce: IndividualNonce, pubkey: PublicKey, cache: KeyAggCache): Boolean {
79+
return Secp256k1.musigPartialSigVerify(psig.toByteArray(), pubnonce.toByteArray(), pubkey.value.toByteArray(), cache.data.toByteArray(), toByteArray()) == 1
80+
}
81+
82+
/**
83+
* @param psigs partial signatures
84+
* @return the aggregate of all input partial signatures
85+
*/
86+
public fun add(psigs: List<ByteVector32>): ByteVector64 {
87+
return Secp256k1.musigPartialSigAgg(toByteArray(), psigs.map { it.toByteArray() }.toTypedArray()).byteVector64()
88+
}
89+
90+
public companion object {
91+
/**
92+
* @param aggregatedNonce aggregated public nonce
93+
* @param msg message to sign
94+
* @param cache key aggregation cache
95+
* @return a Musig signing session
96+
*/
97+
@JvmStatic
98+
public fun build(aggregatedNonce: AggregatedNonce, msg: ByteVector32, cache: KeyAggCache): Session {
99+
val session = Secp256k1.musigNonceProcess(aggregatedNonce.toByteArray(), msg.toByteArray(), cache.data.toByteArray(), null)
100+
return Session(session.byteVector())
101+
}
102+
}
103+
}
104+
105+
/**
106+
* Musig2 secret nonce. Not meant to be reused !!
107+
*/
108+
public data class SecretNonce(val data: ByteVector) {
109+
public constructor(bin: ByteArray) : this(bin.byteVector())
110+
111+
public constructor(hex: String) : this(Hex.decode(hex))
112+
113+
init {
114+
require(data.size() == Secp256k1.MUSIG2_SECRET_NONCE_SIZE) { "musig2 secret nonce must be ${Secp256k1.MUSIG2_SECRET_NONCE_SIZE} bytes" }
115+
}
116+
117+
public companion object {
118+
/**
119+
* @param sessionId random session id. Must not be reused !!
120+
* @param seckey optional private key
121+
* @param pubkey public key
122+
* @param msg optional message to sign
123+
* @param cache optional key aggregation cache
124+
* @param extraInput optional extra input value
125+
* @return a (secret nonce, public nonce) tuple
126+
*/
127+
@JvmStatic
128+
public fun generate(sessionId: ByteVector32, seckey: PrivateKey?, pubkey: PublicKey, msg: ByteVector32?, cache: KeyAggCache?, extraInput: ByteVector32?): Pair<SecretNonce, IndividualNonce> {
129+
val nonce = Secp256k1.musigNonceGen(sessionId.toByteArray(), seckey?.value?.toByteArray(), pubkey.value.toByteArray(), msg?.toByteArray(), cache?.data?.toByteArray(), extraInput?.toByteArray())
130+
return Pair(SecretNonce(nonce.copyOfRange(0, Secp256k1.MUSIG2_SECRET_NONCE_SIZE)), IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)))
131+
}
132+
}
133+
}
134+
135+
/**
136+
* Musig2 public nonce
137+
*/
138+
public data class IndividualNonce(val data: ByteVector) {
139+
public constructor(bin: ByteArray) : this(bin.byteVector())
140+
141+
public constructor(hex: String) : this(Hex.decode(hex))
142+
143+
init {
144+
require(data.size() == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) { "individual musig2 public nonce must be ${Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE} bytes" }
145+
}
146+
147+
public fun toByteArray(): ByteArray = data.toByteArray()
148+
149+
public companion object {
150+
@JvmStatic
151+
public fun aggregate(nonces: List<IndividualNonce>): AggregatedNonce {
152+
val agg = Secp256k1.musigNonceAgg(nonces.map { it.toByteArray() }.toTypedArray())
153+
return AggregatedNonce(agg)
154+
}
155+
}
156+
}
157+
158+
/**
159+
* Musig2 aggregated nonce
160+
*/
161+
public data class AggregatedNonce(val data: ByteVector) {
162+
public constructor(bin: ByteArray) : this(bin.byteVector())
163+
164+
public constructor(hex: String) : this(Hex.decode(hex))
165+
166+
init {
167+
require(data.size() == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) { "aggregated musig2 public nonce must be ${Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE} bytes" }
168+
}
169+
170+
public fun toByteArray(): ByteArray = data.toByteArray()
171+
}

0 commit comments

Comments
 (0)