diff --git a/build.gradle.kts b/build.gradle.kts index 2517e413..0e1900cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { val currentOs = org.gradle.internal.os.OperatingSystem.current() group = "fr.acinq.bitcoin" -version = "0.17.0-SNAPSHOT" +version = "0.17.0-MUSIG2-SNAPSHOT" repositories { google() @@ -45,7 +45,7 @@ kotlin { } sourceSets { - val secp256k1KmpVersion = "0.13.0" + val secp256k1KmpVersion = "0.14.0" val commonMain by getting { dependencies { diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt index e38ebc98..3b4a6aaf 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt @@ -197,24 +197,6 @@ public object Crypto { return sig } - /** Produce a signature that will be included in the witness of a taproot key path spend. */ - @JvmStatic - public fun signTaprootKeyPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List, sighashType: Int, scriptTree: ScriptTree?, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 { - val data = Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, inputs, sighashType, annex) - val tweak = when (scriptTree) { - null -> TaprootTweak.NoScriptTweak - else -> TaprootTweak.ScriptTweak(scriptTree.hash()) - } - return signSchnorr(data, privateKey, tweak, auxrand32) - } - - /** Produce a signature that will be included in the witness of a taproot script path spend. */ - @JvmStatic - public fun signTaprootScriptPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List, sighashType: Int, tapleaf: ByteVector32, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 { - val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex, inputs, sighashType, tapleaf, annex) - return signSchnorr(data, privateKey, SchnorrTweak.NoTweak, auxrand32) - } - @JvmStatic public fun verifySignatureSchnorr(data: ByteVector32, signature: ByteVector, publicKey: XonlyPublicKey): Boolean { return Secp256k1.verifySchnorr(signature.toByteArray(), data.toByteArray(), publicKey.value.toByteArray()) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt index c16c3f53..3aea069c 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt @@ -798,6 +798,53 @@ public data class Transaction( return hashForSigningSchnorr(tx, inputIndex, inputs, sighashType, SigVersion.SIGVERSION_TAPSCRIPT, tapleaf, annex) } + /** + * Sign a taproot tx input, using the internal key path. + * + * @param privateKey private key. + * @param tx input transaction. + * @param inputIndex index of the tx input that is being signed. + * @param inputs list of all UTXOs spent by this transaction. + * @param sighashType signature hash type, which will be appended to the signature (if not default). + * @param scriptTree tapscript tree of the signed input, if it has script paths. + * @return the schnorr signature of this tx for this specific tx input. + */ + @JvmStatic + public fun signInputTaprootKeyPath(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: List, sighashType: Int, scriptTree: ScriptTree?, annex: ByteVector? = null, auxrand32: ByteVector32? = null): ByteVector64 { + val data = hashForSigningTaprootKeyPath(tx, inputIndex, inputs, sighashType, annex) + val tweak = when (scriptTree) { + null -> Crypto.TaprootTweak.NoScriptTweak + else -> Crypto.TaprootTweak.ScriptTweak(scriptTree.hash()) + } + return Crypto.signSchnorr(data, privateKey, tweak, auxrand32) + } + + /** + * Sign a taproot tx input, using one of its script paths. + * + * @param privateKey private key. + * @param tx input transaction. + * @param inputIndex index of the tx input that is being signed. + * @param inputs list of all UTXOs spent by this transaction. + * @param sighashType signature hash type, which will be appended to the signature (if not default). + * @param tapleaf tapscript leaf hash of the script that is being spent. + * @return the schnorr signature of this tx for this specific tx input and the given script leaf. + */ + @JvmStatic + public fun signInputTaprootScriptPath( + privateKey: PrivateKey, + tx: Transaction, + inputIndex: Int, + inputs: List, + sighashType: Int, + tapleaf: ByteVector32, + annex: ByteVector? = null, + auxrand32: ByteVector32? = null + ): ByteVector64 { + val data = hashForSigningTaprootScriptPath(tx, inputIndex, inputs, sighashType, tapleaf, annex) + return Crypto.signSchnorr(data, privateKey, Crypto.SchnorrTweak.NoTweak, auxrand32) + } + @JvmStatic public fun correctlySpends(tx: Transaction, previousOutputs: Map, scriptFlags: Int) { val prevouts = tx.txIn.map { previousOutputs[it.outPoint]!! } diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt new file mode 100644 index 00000000..9b99c6af --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt @@ -0,0 +1,295 @@ +package fr.acinq.bitcoin.crypto.musig2 + +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.utils.Either +import fr.acinq.bitcoin.utils.flatMap +import fr.acinq.secp256k1.Hex +import fr.acinq.secp256k1.Secp256k1 +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +/** + * Musig2 key aggregation cache: keeps track of an aggregate of public keys, that can optionally be tweaked. + * This should be treated as an opaque blob of data, that doesn't contain any sensitive data and thus can be stored. + */ +public data class KeyAggCache(private val data: ByteVector) { + public constructor(data: ByteArray) : this(data.byteVector()) + + init { + require(data.size() == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) { "musig2 keyagg cache must be ${Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() + + override fun toString(): String = data.toHex() + + /** + * @param tweak tweak to apply. + * @param isXonly true if the tweak is an x-only tweak. + * @return an updated cache and the tweaked aggregated public key, or null if one of the tweaks is invalid. + */ + public fun tweak(tweak: ByteVector32, isXonly: Boolean): Either> = try { + val localCache = toByteArray() + val tweaked = if (isXonly) { + Secp256k1.musigPubkeyXonlyTweakAdd(localCache, tweak.toByteArray()) + } else { + Secp256k1.musigPubkeyTweakAdd(localCache, tweak.toByteArray()) + } + Either.Right(Pair(KeyAggCache(localCache), PublicKey.parse(tweaked))) + } catch (t: Throwable) { + Either.Left(t) + } + + public companion object { + /** + * @param publicKeys public keys to aggregate: callers must verify that all public keys are valid. + * @return an opaque key aggregation cache and the aggregated public key. + */ + @JvmStatic + public fun create(publicKeys: List): Pair { + require(publicKeys.all { it.isValid() }) { "some of the public keys provided are not valid" } + val localCache = ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) + val aggkey = Secp256k1.musigPubkeyAgg(publicKeys.map { it.value.toByteArray() }.toTypedArray(), localCache) + return Pair(XonlyPublicKey(aggkey.byteVector32()), KeyAggCache(localCache.byteVector())) + } + } +} + +/** + * Musig2 signing session context that can be used to create partial signatures and aggregate them. + */ +public data class Session(private val data: ByteVector, private val keyAggCache: KeyAggCache) { + init { + require(data.size() == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE) { "musig2 session must be ${Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() + + /** + * @param secretNonce signer's secret nonce (see [SecretNonce.generate]). + * @param privateKey signer's private key. + * @return a musig2 partial signature. + */ + public fun sign(secretNonce: SecretNonce, privateKey: PrivateKey): ByteVector32 { + return Secp256k1.musigPartialSign(secretNonce.data.toByteArray(), privateKey.value.toByteArray(), keyAggCache.toByteArray(), this.toByteArray()).byteVector32() + } + + /** + * @param partialSig musig2 partial signature. + * @param publicNonce individual public nonce of the signing participant. + * @param publicKey individual public key of the signing participant. + * @return true if the partial signature is valid. + */ + public fun verify(partialSig: ByteVector32, publicNonce: IndividualNonce, publicKey: PublicKey): Boolean = try { + Secp256k1.musigPartialSigVerify(partialSig.toByteArray(), publicNonce.toByteArray(), publicKey.value.toByteArray(), keyAggCache.toByteArray(), this.toByteArray()) == 1 + } catch (t: Throwable) { + false + } + + /** + * Aggregate partial signatures from all participants into a single schnorr signature. Callers should verify the + * resulting signature, which may be invalid without raising an error here (for example if the set of partial + * signatures is valid but incomplete). + * + * @param partialSigs partial signatures from all signing participants. + * @return the aggregate signature of all input partial signatures or null if a partial signature is invalid. + */ + public fun aggregateSigs(partialSigs: List): Either = try { + Either.Right(Secp256k1.musigPartialSigAgg(this.toByteArray(), partialSigs.map { it.toByteArray() }.toTypedArray()).byteVector64()) + } catch (t: Throwable) { + Either.Left(t) + } + + public companion object { + /** + * @param aggregatedNonce aggregated public nonce. + * @param message message that will be signed. + * @param keyAggCache key aggregation cache. + * @return a musig2 signing session. + */ + @JvmStatic + public fun create(aggregatedNonce: AggregatedNonce, message: ByteVector32, keyAggCache: KeyAggCache): Session { + val session = Secp256k1.musigNonceProcess(aggregatedNonce.toByteArray(), message.toByteArray(), keyAggCache.toByteArray()) + return Session(session.byteVector(), keyAggCache) + } + } +} + +/** + * Musig2 secret nonce, that should be treated as a private opaque blob. + * This nonce must never be persisted or reused across signing sessions. + */ +public data class SecretNonce(internal val data: ByteVector) { + public constructor(bin: ByteArray) : this(bin.byteVector()) + public constructor(hex: String) : this(Hex.decode(hex)) + + init { + require(data.size() == Secp256k1.MUSIG2_SECRET_NONCE_SIZE) { "musig2 secret nonce must be ${Secp256k1.MUSIG2_SECRET_NONCE_SIZE} bytes" } + } + + override fun toString(): String = "" + + public companion object { + /** + * Generate a secret nonce to be used in a musig2 signing session. + * This nonce must never be persisted or reused across signing sessions. + * All optional arguments exist to enrich the quality of the randomness used, which is critical for security. + * + * @param sessionId unique session ID. + * @param privateKey (optional) signer's private key. + * @param publicKey signer's public key. + * @param message (optional) message that will be signed, if already known. + * @param keyAggCache (optional) key aggregation cache data from the signing session. + * @param extraInput (optional) additional random data. + * @return secret nonce and the corresponding public nonce. + */ + @JvmStatic + public fun generate(sessionId: ByteVector32, privateKey: PrivateKey?, publicKey: PublicKey, message: ByteVector32?, keyAggCache: KeyAggCache?, extraInput: ByteVector32?): Pair { + privateKey?.let { require(it.publicKey() == publicKey) { "if the private key is provided, it must match the public key" } } + val nonce = Secp256k1.musigNonceGen(sessionId.toByteArray(), privateKey?.value?.toByteArray(), publicKey.value.toByteArray(), message?.toByteArray(), keyAggCache?.toByteArray(), extraInput?.toByteArray()) + val secretNonce = SecretNonce(nonce.copyOfRange(0, Secp256k1.MUSIG2_SECRET_NONCE_SIZE)) + val publicNonce = IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)) + return Pair(secretNonce, publicNonce) + } + } +} + +/** + * Musig2 public nonce, that must be shared with other participants in the signing session. + * It contains two elliptic curve points, but should be treated as an opaque blob. + */ +public data class IndividualNonce(val data: ByteVector) { + public constructor(bin: ByteArray) : this(bin.byteVector()) + public constructor(hex: String) : this(Hex.decode(hex)) + + init { + require(data.size() == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) { "individual musig2 public nonce must be ${Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() + + override fun toString(): String = data.toHex() + + public companion object { + /** + * Aggregate public nonces from all participants of a signing session. + * Returns null if one of the nonces provided is invalid. + */ + @JvmStatic + public fun aggregate(nonces: List): Either = try { + val agg = Secp256k1.musigNonceAgg(nonces.map { it.toByteArray() }.toTypedArray()) + Either.Right(AggregatedNonce(agg)) + } catch (t: Throwable) { + Either.Left(t) + } + } +} + +/** + * Musig2 aggregate public nonce from all participants of a signing session. + */ +public data class AggregatedNonce(val data: ByteVector) { + public constructor(bin: ByteArray) : this(bin.byteVector()) + public constructor(hex: String) : this(Hex.decode(hex)) + + init { + require(data.size() == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE) { "aggregated musig2 public nonce must be ${Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE} bytes" } + } + + public fun toByteArray(): ByteArray = data.toByteArray() + + override fun toString(): String = data.toHex() +} + +/** + * This object contain helper functions to use musig2 in the context of spending taproot outputs. + * In order to provide a simpler API, some operations are internally duplicated: if performance is an issue, you should + * consider using the lower-level APIs directly (see [Session] and [KeyAggCache]). + */ +public object Musig2 { + /** + * Aggregate the public keys of a musig2 session into a single public key. + * Note that this function doesn't apply any tweak: when used for taproot, it computes the internal public key, not + * the public key exposed in the script (which is tweaked with the script tree). + * + * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. + */ + @JvmStatic + public fun aggregateKeys(publicKeys: List): XonlyPublicKey = KeyAggCache.create(publicKeys).first + + /** + * @param sessionId a random, unique session ID. + * @param privateKey signer's private key. + * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. + */ + @JvmStatic + public fun generateNonce(sessionId: ByteVector32, privateKey: PrivateKey, publicKeys: List): Pair { + val (_, keyAggCache) = KeyAggCache.create(publicKeys) + return SecretNonce.generate(sessionId, privateKey, privateKey.publicKey(), message = null, keyAggCache, extraInput = null) + } + + private fun taprootSession(tx: Transaction, inputIndex: Int, inputs: List, publicKeys: List, publicNonces: List, scriptTree: ScriptTree?): Either { + return IndividualNonce.aggregate(publicNonces).flatMap { aggregateNonce -> + val (aggregatePublicKey, keyAggCache) = KeyAggCache.create(publicKeys) + val tweak = when (scriptTree) { + null -> aggregatePublicKey.tweak(Crypto.TaprootTweak.NoScriptTweak) + else -> aggregatePublicKey.tweak(Crypto.TaprootTweak.ScriptTweak(scriptTree)) + } + keyAggCache.tweak(tweak, isXonly = true).map { tweakedKeyAggCache -> + val txHash = Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, inputs, SigHash.SIGHASH_DEFAULT) + Session.create(aggregateNonce, txHash, tweakedKeyAggCache.first) + } + } + } + + /** + * Create a partial musig2 signature for the given taproot input key path. + * + * @param privateKey private key of the signing participant. + * @param tx transaction spending the target taproot input. + * @param inputIndex index of the taproot input to spend. + * @param inputs all inputs of the spending transaction. + * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid. + * @param secretNonce secret nonce of the signing participant. + * @param publicNonces public nonces of all participants of the musig2 session. + * @param scriptTree tapscript tree of the taproot input, if it has script paths. + */ + @JvmStatic + public fun signTaprootInput( + privateKey: PrivateKey, + tx: Transaction, + inputIndex: Int, + inputs: List, + publicKeys: List, + secretNonce: SecretNonce, + publicNonces: List, + scriptTree: ScriptTree? + ): Either { + return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).map { it.sign(secretNonce, privateKey) } + } + + /** + * Aggregate partial musig2 signatures into a valid schnorr signature for the given taproot input key path. + * + * @param partialSigs partial musig2 signatures of all participants of the musig2 session. + * @param tx transaction spending the target taproot input. + * @param inputIndex index of the taproot input to spend. + * @param inputs all inputs of the spending transaction. + * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid. + * @param publicNonces public nonces of all participants of the musig2 session. + * @param scriptTree tapscript tree of the taproot input, if it has script paths. + */ + @JvmStatic + public fun aggregateTaprootSignatures( + partialSigs: List, + tx: Transaction, + inputIndex: Int, + inputs: List, + publicKeys: List, + publicNonces: List, + scriptTree: ScriptTree? + ): Either { + return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).flatMap { it.aggregateSigs(partialSigs) } + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt deleted file mode 100644 index 9640ff04..00000000 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/musig2/Musig2.kt +++ /dev/null @@ -1,294 +0,0 @@ -package fr.acinq.bitcoin.musig2 - -import fr.acinq.bitcoin.* -import fr.acinq.bitcoin.crypto.Pack -import fr.acinq.secp256k1.Hex -import fr.acinq.secp256k1.Secp256k1 -import kotlin.experimental.xor -import kotlin.jvm.JvmStatic - - -/** - * Key Aggregation Context - * Holds a public key aggregate that can optionally be tweaked - * @param Q aggregated public key - * @param gacc G accumulator - * @param tacc tweak accumulator - */ -public data class KeyAggCtx(val Q: PublicKey, val gacc: Boolean, val tacc: ByteVector32) { - public fun tweak(tweak: ByteVector32, isXonly: Boolean): KeyAggCtx { - require(tweak == ByteVector32.Zeroes || PrivateKey(tweak).isValid()) { "invalid tweak" } - return if (isXonly && !Q.isEven()) { - val Q1 = PublicKey.parse(Secp256k1.pubKeyTweakAdd(Q.unaryMinus().toUncompressedBin(), tweak.toByteArray())) - KeyAggCtx(Q1, !gacc, minus(tweak, tacc)) - } else { - val Q1 = PublicKey.parse(Secp256k1.pubKeyTweakAdd(Q.toUncompressedBin(), tweak.toByteArray())) - KeyAggCtx(Q1, gacc, add(tweak, tacc)) - } - } -} - -public object Musig2 { - @JvmStatic - public fun keyAgg(pubkeys: List): KeyAggCtx { - val pk2 = getSecondKey(pubkeys) - val a = pubkeys.map { keyAggCoeffInternal(pubkeys, it, pk2) } - val Q = pubkeys.zip(a).map { it.first.times(PrivateKey(it.second)) }.reduce { p1, p2 -> p1 + p2 } - return KeyAggCtx(Q, true, ByteVector32.Zeroes) - } - - @JvmStatic - public fun keySort(pubkeys: List): List = pubkeys.sortedWith { a, b -> LexicographicalOrdering.compare(a, b) } -} - -/** - * Musig2 secret nonce. Not meant to be reused !! - */ -public data class SecretNonce(val data: ByteVector) { - public constructor(bin: ByteArray) : this(bin.byteVector()) - - public constructor(hex: String) : this(Hex.decode(hex)) - - init { - require(data.size() == 32 + 32 + 33) { "musig2 secret nonce must be 97 bytes" } - } - - internal val p1: PrivateKey = PrivateKey(data.take(32)) - internal val p2: PrivateKey = PrivateKey(data.drop(32).take(32)) - internal val pk: PublicKey = PublicKey(data.takeRight(33)) - public fun publicNonce(): IndividualNonce = IndividualNonce(p1.publicKey().value + p2.publicKey().value) - - public companion object { - /** - * @param sk optional private key - * @param pk public key - * @param aggpk optional aggregated public key - * @param msg optional message - * @param extraInput optional extra input - * @param randprime random value - * @return a Musig2 secret nonce - */ - @JvmStatic - public fun generate(sk: PrivateKey?, pk: PublicKey, aggpk: XonlyPublicKey?, msg: ByteArray?, extraInput: ByteArray?, randprime: ByteVector32): SecretNonce { - - fun xor(a: ByteVector32, b: ByteVector32): ByteVector32 { - val result = ByteArray(32) - for (i in 0..31) { - result[i] = a[i].xor(b[i]) - } - return result.byteVector32() - } - - val rand = if (sk != null) { - xor(sk.value, Crypto.taggedHash(randprime.toByteArray(), "MuSig/aux")) - } else { - randprime - } - val aggpk1 = aggpk?.value?.toByteArray() ?: ByteArray(0) - val extraInput1 = extraInput ?: ByteArray(0) - val tmp = rand.toByteArray() + - ByteArray(1) { pk.value.size().toByte() } + pk.value.toByteArray() + - ByteArray(1) { aggpk1.size.toByte() } + aggpk1 + - if (msg != null) { - ByteArray(1) { 1 } + Pack.writeInt64BE(msg.size.toLong()) + msg - } else { - ByteArray(1) { 0 } - } + - Pack.writeInt32BE(extraInput1.size) + extraInput1 - val k1 = Crypto.taggedHash(tmp + ByteArray(1) { 0 }, "MuSig/nonce") - require(k1 != ByteVector32.Zeroes) - val k2 = Crypto.taggedHash(tmp + ByteArray(1) { 1 }, "MuSig/nonce") - require(k2 != ByteVector32.Zeroes) - val secnonce = SecretNonce(PrivateKey(k1).value + PrivateKey(k2).value + pk.value) - return secnonce - } - } -} - -/** - * Musig2 public nonce - */ -public data class IndividualNonce(val data: ByteVector) { - public constructor(bin: ByteArray) : this(bin.byteVector()) - - public constructor(hex: String) : this(Hex.decode(hex)) - - init { - require(data.size() == 66) { "individual musig2 public nonce must be 66 bytes" } - } - - internal val P1: PublicKey = PublicKey(data.take(33)) - - internal val P2: PublicKey = PublicKey(data.drop(33)) - public fun isValid(): Boolean = P1.isValid() && P2.isValid() - - public fun toByteArray(): ByteArray = data.toByteArray() - - public companion object { - @JvmStatic - public fun aggregate(nonces: List): AggregatedNonce { - for (i in nonces.indices) { - require(nonces[i].isValid()) { "invalid nonce at index $i" } - } - val np: PublicKey? = null - val R1 = nonces.map { it.P1 }.fold(np) { a, b -> add(a, b) } - val R2 = nonces.map { it.P2 }.fold(np) { a, b -> add(a, b) } - return AggregatedNonce(R1, R2) - } - } -} - -/** - * Aggregated nonce. - * The sum of 2 public keys could be 0 (P + (-P)) which we represent with null (0 is a valid point but not a valid public key) - */ -public data class AggregatedNonce(val data: ByteVector) { - public constructor(bin: ByteArray) : this(bin.byteVector()) - - public constructor(hex: String) : this(Hex.decode(hex)) - - internal constructor(p1: PublicKey?, p2: PublicKey?) : this((p1?.value?.toByteArray() ?: ByteArray(33)) + (p2?.value?.toByteArray() ?: ByteArray(33))) - - init { - require(data.size() == 66) { "aggregated musig2 public nonce must be 66 bytes" } - } - - internal val P1: PublicKey? = run { - val bin = data.take(33) - if (bin.contentEquals(ByteArray(33))) null else PublicKey(bin) - } - - internal val P2: PublicKey? = run { - val bin = data.drop(33) - if (bin.contentEquals(ByteArray(33))) null else PublicKey(bin) - } - - public fun isValid(): Boolean = (P1?.isValid() ?: true) && (P2?.isValid() ?: true) - - public fun toByteArray(): ByteArray = data.toByteArray() -} - -internal fun add(a: ByteVector32, b: ByteVector32): ByteVector32 = when { - a == ByteVector32.Zeroes -> b - b == ByteVector32.Zeroes -> a - else -> (PrivateKey(a) + PrivateKey(b)).value -} - -internal fun unaryMinus(a: ByteVector32): ByteVector32 = when { - a == ByteVector32.Zeroes -> a - else -> PrivateKey(a).unaryMinus().value -} - -internal fun minus(a: ByteVector32, b: ByteVector32): ByteVector32 = add(a, unaryMinus(b)) -internal fun mul(a: ByteVector32, b: ByteVector32): ByteVector32 = when { - a == ByteVector32.Zeroes || b == ByteVector32.Zeroes -> ByteVector32.Zeroes - else -> (PrivateKey(a) * PrivateKey(b)).value -} - -internal fun add(a: PublicKey?, b: PublicKey?): PublicKey? = when { - a == null -> b - b == null -> a - a.xOnly() == b.xOnly() && (a.isEven() != b.isEven()) -> null - else -> a + b -} - - -internal fun mul(a: PublicKey?, b: PrivateKey): PublicKey? = a?.times(b) - -/** - * Musig2 signing session context - * @param aggnonce aggregated public nonce - * @param pubkeys signer public keys - * @param tweaks optional tweaks to apply to the aggregated public key - * @param message message to sign - */ -public data class SessionCtx(val aggnonce: AggregatedNonce, val pubkeys: List, val tweaks: List>, val message: ByteVector) { - private fun build(): SessionValues { - val keyAggCtx0 = Musig2.keyAgg(pubkeys) - val keyAggCtx = tweaks.fold(keyAggCtx0) { ctx, tweak -> ctx.tweak(tweak.first, tweak.second) } - val (Q, gacc, tacc) = keyAggCtx - val b = PrivateKey(Crypto.taggedHash((aggnonce.toByteArray().byteVector() + Q.xOnly().value + message).toByteArray(), "MuSig/noncecoef")) - val R = add(aggnonce.P1, mul(aggnonce.P2, b)) ?: PublicKey.Generator - val e = Crypto.taggedHash((R.xOnly().value + Q.xOnly().value + message).toByteArray(), "BIP0340/challenge") - return SessionValues(Q, gacc, tacc, b, R, PrivateKey(e)) - } - - private fun getSessionKeyAggCoeff(P: PublicKey): PrivateKey { - require(pubkeys.contains(P)) { "signer's pubkey is not present" } - return keyAggCoeff(pubkeys, P) - } - - /** - * @param secnonce secret nonce - * @param sk private key - * @return a Musig2 partial signature, or null if the nonce does not match the private key or the partial signature cannot be verified - */ - public fun sign(secnonce: SecretNonce, sk: PrivateKey): ByteVector32? = runCatching { - val (Q, gacc, _, b, R, e) = build() - val (k1, k2) = if (R.isEven()) Pair(secnonce.p1, secnonce.p2) else Pair(-secnonce.p1, -secnonce.p2) - val P = sk.publicKey() - require(P == secnonce.pk) { "nonce and private key mismatch" } - val a = getSessionKeyAggCoeff(P) - val d = if (Q.isEven() == gacc) sk else -sk - val s = k1 + b * k2 + e * a * d - require(partialSigVerify(s.value, secnonce.publicNonce(), sk.publicKey())) { "partial signature verification failed" } - s.value - }.getOrNull() - - /** - * @param psig Musig2 partial signature - * @param pubnonce public nonce - * @param pk public key - * @return true if the partial signature has been verified (in the context of a specific signing session) - */ - public fun partialSigVerify(psig: ByteVector32, pubnonce: IndividualNonce, pk: PublicKey): Boolean { - val (Q, gacc, _, b, R, e) = build() - val Rstar = add(pubnonce.P1, mul(pubnonce.P2, b)) ?: PublicKey.Generator - val Re = if (R.isEven()) Rstar else -Rstar - val a = getSessionKeyAggCoeff(pk) - val gprime = if (Q.isEven()) gacc else !gacc - val check = if (gprime) Re + pk * e * a else Re - pk * e * a - return PrivateKey(psig).publicKey() == check - } - - /** - * @param psigs list of partial signatures - * @return an aggregated signature, which is a valid Schnorr signature for the matching aggregated public key - * or null is one of the partial signatures is not valid - */ - public fun partialSigAgg(psigs: List): ByteVector64? = runCatching { - val (Q, _, tacc, _, R, e) = build() - for (i in psigs.indices) { - require(PrivateKey(psigs[i]).isValid()) { "invalid partial signature at index $i" } - } - val s = psigs.reduce { a, b -> add(a, b) } - val s1 = if (Q.isEven()) add(s, mul(e.value, tacc)) else minus(s, mul(e.value, tacc)) - val sig = ByteVector64(R.xOnly().value + s1) - sig - }.getOrNull() - - public companion object { - private data class SessionValues(val Q: PublicKey, val gacc: Boolean, val tacc: ByteVector32, val b: PrivateKey, val R: PublicKey, val e: PrivateKey) - } -} - -internal fun getSecondKey(pubkeys: List): PublicKey { - return pubkeys.drop(1).find { it != pubkeys[0] } ?: PublicKey(ByteArray(33)) -} - -internal fun hashKeys(pubkeys: List): ByteVector32 { - val concat = pubkeys.map { it.value }.reduce { a, b -> a + b } - return Crypto.taggedHash(concat.toByteArray(), "KeyAgg list") -} - -internal fun keyAggCoeffInternal(pubkeys: List, pk: PublicKey, pk2: PublicKey): ByteVector32 { - return if (pk == pk2) { - ByteVector32.One.reversed() - } else { - Crypto.taggedHash(hashKeys(pubkeys).toByteArray() + pk.value.toByteArray(), "KeyAgg coefficient") - } -} - -internal fun keyAggCoeff(pubkeys: List, pk: PublicKey): PrivateKey { - return PrivateKey(keyAggCoeffInternal(pubkeys, pk, getSecondKey(pubkeys))) -} diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt deleted file mode 100644 index 0e66b577..00000000 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/Musig2TestsCommon.kt +++ /dev/null @@ -1,389 +0,0 @@ -package fr.acinq.bitcoin - -import fr.acinq.bitcoin.musig2.* -import fr.acinq.bitcoin.reference.TransactionTestsCommon -import fr.acinq.secp256k1.Hex -import kotlinx.serialization.json.* -import kotlin.random.Random -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFails -import kotlin.test.assertTrue - -class Musig2TestsCommon { - @Test - fun `sort public keys`() { - val tests = TransactionTestsCommon.readData("musig2/key_sort_vectors.json") - val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } - val expected = tests.jsonObject["sorted_pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } - assertEquals(expected, Musig2.keySort(pubkeys)) - } - - @Test - fun `aggregate public keys`() { - val tests = TransactionTestsCommon.readData("musig2/key_agg_vectors.json") - val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } - val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } - - tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val expected = XonlyPublicKey(ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content)) - val ctx = Musig2.keyAgg(keyIndices.map { pubkeys[it] }) - assertEquals(expected, ctx.Q.xOnly()) - } - tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } - assertFails { - var ctx = Musig2.keyAgg(keyIndices.map { pubkeys[it] }) - tweakIndices.zip(isXonly).forEach { ctx = ctx.tweak(tweaks[it.first], it.second) } - } - } - } - - @Test - fun `generate secret nonce`() { - val tests = TransactionTestsCommon.readData("musig2/nonce_gen_vectors.json") - tests.jsonObject["test_cases"]!!.jsonArray.forEach { - val randprime = ByteVector32.fromValidHex(it.jsonObject["rand_"]!!.jsonPrimitive.content) - val sk = it.jsonObject["sk"]?.jsonPrimitive?.contentOrNull?.let { PrivateKey.fromHex(it) } - val pk = PublicKey.fromHex(it.jsonObject["pk"]!!.jsonPrimitive.content) - val aggpk = it.jsonObject["aggpk"]?.jsonPrimitive?.contentOrNull?.let { XonlyPublicKey(ByteVector32.fromValidHex(it)) } - val msg = it.jsonObject["msg"]?.jsonPrimitive?.contentOrNull?.let { Hex.decode(it) } - val extraInput = it.jsonObject["extra_in"]?.jsonPrimitive?.contentOrNull?.let { Hex.decode(it) } - val expectedSecnonce = SecretNonce(it.jsonObject["expected_secnonce"]!!.jsonPrimitive.content) - val expectedPubnonce = IndividualNonce(it.jsonObject["expected_pubnonce"]!!.jsonPrimitive.content) - val secnonce = SecretNonce.generate(sk, pk, aggpk, msg, extraInput, randprime) - assertEquals(expectedSecnonce, secnonce) - assertEquals(expectedPubnonce, secnonce.publicNonce()) - } - } - - @Test - fun `aggregate nonces`() { - val tests = TransactionTestsCommon.readData("musig2/nonce_agg_vectors.json") - val nonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } - tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { - val nonceIndices = it.jsonObject["pnonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val expected = AggregatedNonce(it.jsonObject["expected"]!!.jsonPrimitive.content) - val agg = IndividualNonce.aggregate(nonceIndices.map { nonces[it] }) - assertEquals(expected, agg) - } - tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { - val nonceIndices = it.jsonObject["pnonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - assertFails { - IndividualNonce.aggregate(nonceIndices.map { nonces[it] }) - } - } - } - - @Test - fun sign() { - val tests = TransactionTestsCommon.readData("musig2/sign_verify_vectors.json") - val sk = PrivateKey.fromHex(tests.jsonObject["sk"]!!.jsonPrimitive.content) - val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } - val secnonces = tests.jsonObject["secnonces"]!!.jsonArray.map { SecretNonce(it.jsonPrimitive.content) } - val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } - val aggnonces = tests.jsonObject["aggnonces"]!!.jsonArray.map { AggregatedNonce(it.jsonPrimitive.content) } - val msgs = tests.jsonObject["msgs"]!!.jsonArray.map { ByteVector(it.jsonPrimitive.content) } - - tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val expected = ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) - val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int - val agg = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }) - assertEquals(aggnonces[it.jsonObject["aggnonce_index"]!!.jsonPrimitive.int], agg) - val ctx = SessionCtx( - agg, - keyIndices.map { pubkeys[it] }, - listOf(), - msgs[it.jsonObject["msg_index"]!!.jsonPrimitive.int] - ) - val psig = ctx.sign(secnonces[keyIndices[signerIndex]], sk)!! - assertEquals(expected, psig) - assertTrue { - ctx.partialSigVerify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]]) - } - } - - tests.jsonObject["sign_error_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val ctx = SessionCtx( - aggnonces[it.jsonObject["aggnonce_index"]!!.jsonPrimitive.int], - keyIndices.map { pubkeys[it] }, - listOf(), - msgs[it.jsonObject["msg_index"]!!.jsonPrimitive.int] - ) - require(ctx.sign(secnonces[it.jsonObject["secnonce_index"]!!.jsonPrimitive.int], sk) == null) - } - } - - @Test - fun `aggregate signatures`() { - val tests = TransactionTestsCommon.readData("musig2/sig_agg_vectors.json") - val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } - val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } - val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } - val psigs = tests.jsonObject["psigs"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } - val msg = ByteVector.fromHex(tests.jsonObject["msg"]!!.jsonPrimitive.content) - - tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val psigIndices = it.jsonObject["psig_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val expected = ByteVector64.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) - val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }) - val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } - assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) - val ctx = SessionCtx( - aggnonce, - keyIndices.map { pubkeys[it] }, - tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, - msg - ) - val aggsig = ctx.partialSigAgg(psigIndices.map { psigs[it] })!! - assertEquals(expected, aggsig) - } - tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val psigIndices = it.jsonObject["psig_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }) - val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } - assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) - val ctx = SessionCtx( - aggnonce, - keyIndices.map { pubkeys[it] }, - tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, - msg - ) - require(ctx.partialSigAgg(psigIndices.map { psigs[it] }) == null) - } - } - - @Test - fun `tweak tests`() { - val tests = TransactionTestsCommon.readData("musig2/tweak_vectors.json") - val sk = PrivateKey.fromHex(tests.jsonObject["sk"]!!.jsonPrimitive.content) - val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } - val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } - val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } - val msg = ByteVector.fromHex(tests.jsonObject["msg"]!!.jsonPrimitive.content) - - val secnonce = SecretNonce(tests.jsonObject["secnonce"]!!.jsonPrimitive.content) - val aggnonce = AggregatedNonce(tests.jsonObject["aggnonce"]!!.jsonPrimitive.content) - - assertEquals(pubkeys[0], sk.publicKey()) - assertEquals(pnonces[0], secnonce.publicNonce()) - assertEquals(aggnonce, IndividualNonce.aggregate(listOf(pnonces[0], pnonces[1], pnonces[2]))) - - tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val expected = ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) - assertEquals(aggnonce, IndividualNonce.aggregate(nonceIndices.map { pnonces[it] })) - val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } - val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int - val ctx = SessionCtx( - aggnonce, - keyIndices.map { pubkeys[it] }, - tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, - msg - ) - val psig = ctx.sign(secnonce, sk)!! - assertEquals(expected, psig) - assertTrue { ctx.partialSigVerify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]]) } - } - tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { - val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - assertEquals(aggnonce, IndividualNonce.aggregate(nonceIndices.map { pnonces[it] })) - val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } - val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } - val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int - assertFails { - val ctx = SessionCtx( - aggnonce, - keyIndices.map { pubkeys[it] }, - tweakIndices.zip(isXonly).map { tweaks[it.first] to it.second }, - msg - ) - val psig = ctx.sign(secnonce, sk)!! - ctx.partialSigVerify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]]) - } - } - } - - @Test - fun `simple musig2 example`() { - val random = Random.Default - val msg = random.nextBytes(32).byteVector32() - - val privkeys = listOf( - PrivateKey(ByteArray(32) { 1 }), - PrivateKey(ByteArray(32) { 2 }), - PrivateKey(ByteArray(32) { 3 }), - ) - val pubkeys = privkeys.map { it.publicKey() } - - val plainTweak = ByteVector32("this could be a BIP32 tweak....".encodeToByteArray() + ByteArray(1)) - val xonlyTweak = ByteVector32("this could be a taproot tweak..".encodeToByteArray() + ByteArray(1)) - - val aggsig = run { - val secnonces = privkeys.map { - SecretNonce.generate(it, it.publicKey(), null, null, null, random.nextBytes(32).byteVector32()) - } - - val pubnonces = secnonces.map { it.publicNonce() } - - // aggregate public nonces - val aggnonce = IndividualNonce.aggregate(pubnonces) - - // create a signing session - val ctx = SessionCtx( - aggnonce, - pubkeys, - listOf(Pair(plainTweak, false), Pair(xonlyTweak, true)), - msg - ) - - // create partial signatures - val psigs = privkeys.indices.map { - ctx.sign(secnonces[it], privkeys[it])!! - } - - // verify partial signatures - pubkeys.indices.forEach { - assertTrue(ctx.partialSigVerify(psigs[it], pubnonces[it], pubkeys[it])) - } - - // aggregate partial signatures - ctx.partialSigAgg(psigs)!! - } - - // aggregate public keys - val aggpub = Musig2.keyAgg(pubkeys) - .tweak(plainTweak, false) - .tweak(xonlyTweak, true) - - // check that the aggregated signature is a valid, plain Schnorr signature for the aggregated public key - assertTrue(Crypto.verifySignatureSchnorr(msg, aggsig, aggpub.Q.xOnly())) - } - - @Test - fun `use musig2 to replace multisig 2-of-2`() { - val alicePrivKey = PrivateKey(ByteArray(32) { 1 }) - val alicePubKey = alicePrivKey.publicKey() - val bobPrivKey = PrivateKey(ByteArray(32) { 2 }) - val bobPubKey = bobPrivKey.publicKey() - - // Alice and Bob exchange public keys and agree on a common aggregated key - val internalPubKey = Musig2.keyAgg(listOf(alicePubKey, bobPubKey)).Q.xOnly() - // we use the standard BIP86 tweak - val commonPubKey = internalPubKey.outputKey(Crypto.TaprootTweak.NoScriptTweak).first - - // this tx sends to a standard p2tr(commonPubKey) script - val tx = Transaction(2, listOf(), listOf(TxOut(Satoshi(10000), Script.pay2tr(commonPubKey))), 0) - - // this is how Alice and Bob would spend that tx - val spendingTx = Transaction(2, listOf(TxIn(OutPoint(tx, 0), sequence = 0)), listOf(TxOut(Satoshi(10000), Script.pay2wpkh(alicePubKey))), 0) - - val commonSig = run { - val random = Random.Default - val aliceNonce = SecretNonce.generate(alicePrivKey, alicePubKey, commonPubKey, null, null, random.nextBytes(32).byteVector32()) - val bobNonce = SecretNonce.generate(bobPrivKey, bobPubKey, commonPubKey, null, null, random.nextBytes(32).byteVector32()) - - val aggnonce = IndividualNonce.aggregate(listOf(aliceNonce.publicNonce(), bobNonce.publicNonce())) - val msg = Transaction.hashForSigningTaprootKeyPath(spendingTx, 0, listOf(tx.txOut[0]), SigHash.SIGHASH_DEFAULT) - - // we use the same ctx for Alice and Bob, they both know all the public keys that are used here - val ctx = SessionCtx( - aggnonce, - listOf(alicePubKey, bobPubKey), - listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.NoScriptTweak), true)), - msg - ) - val aliceSig = ctx.sign(aliceNonce, alicePrivKey)!! - val bobSig = ctx.sign(bobNonce, bobPrivKey)!! - ctx.partialSigAgg(listOf(aliceSig, bobSig))!! - } - - // this tx looks like any other tx that spends a p2tr output, with a single signature - val signedSpendingTx = spendingTx.updateWitness(0, ScriptWitness(listOf(commonSig))) - Transaction.correctlySpends(signedSpendingTx, tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - - @Test - fun `swap-in-potentiam example with musig2 and taproot`() { - val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) - val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) - val userRefundPrivateKey = PrivateKey(ByteArray(32) { 3 }) - val refundDelay = 25920 - - val random = Random.Default - - // the redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)) - // it does not depend upon the user's or server's key, just the user's refund key and the refund delay - val redeemScript = listOf(OP_PUSHDATA(userRefundPrivateKey.publicKey().xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) - val scriptTree = ScriptTree.Leaf(0, redeemScript) - - // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - val internalPubKey = Musig2.keyAgg(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())).Q.xOnly() - val pubkeyScript = Script.pay2tr(internalPubKey, scriptTree) - - val swapInTx = Transaction( - version = 2, - txIn = listOf(), - txOut = listOf(TxOut(Satoshi(10000), pubkeyScript)), - lockTime = 0 - ) - - // The transaction can be spent if the user and the server produce a signature. - run { - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = TxIn.SEQUENCE_FINAL)), - txOut = listOf(TxOut(Satoshi(10000), Script.pay2wpkh(userPrivateKey.publicKey()))), - lockTime = 0 - ) - // this is the beginning of an interactive musig2 signing session. if user and server are disconnected before they have exchanged partial - // signatures they will have to start again with fresh nonces - val userNonce = SecretNonce.generate(userPrivateKey, userPrivateKey.publicKey(), internalPubKey, null, null, random.nextBytes(32).byteVector32()) - val serverNonce = SecretNonce.generate(serverPrivateKey, serverPrivateKey.publicKey(), internalPubKey, null, null, random.nextBytes(32).byteVector32()) - - val txHash = Transaction.hashForSigningTaprootKeyPath(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT) - val commonNonce = IndividualNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce.publicNonce())) - val ctx = SessionCtx( - commonNonce, - listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey()), - listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(scriptTree)), true)), - txHash - ) - - val userSig = ctx.sign(userNonce, userPrivateKey)!! - val serverSig = ctx.sign(serverNonce, serverPrivateKey)!! - val commonSig = ctx.partialSigAgg(listOf(userSig, serverSig))!! - val signedTx = tx.updateWitness(0, Script.witnessKeyPathPay2tr(commonSig)) - Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - - // Or it can be spent with only the user's signature, after a delay. - run { - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())), - txOut = listOf(TxOut(Satoshi(10000), Script.pay2wpkh(userPrivateKey.publicKey()))), - lockTime = 0 - ) - val sig = Crypto.signTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(internalPubKey, scriptTree, ScriptWitness(listOf(sig)), scriptTree) - val signedTx = tx.updateWitness(0, witness) - Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - } -} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt index 69b35640..390a1aa4 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt @@ -95,7 +95,7 @@ class TaprootTestsCommon { 0 ) val sigHashType = 0 - val sig = Crypto.signTaprootKeyPath(privateKey, tx1, 0, listOf(tx.txOut[1]), sigHashType, scriptTree = null) + val sig = Transaction.signInputTaprootKeyPath(privateKey, tx1, 0, listOf(tx.txOut[1]), sigHashType, scriptTree = null) val tx2 = tx1.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) Transaction.correctlySpends(tx2, tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -193,7 +193,7 @@ class TaprootTestsCommon { ) // compute all 3 signatures - val sigs = privs.map { Crypto.signTaprootScriptPath(it, tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) } + val sigs = privs.map { Transaction.signInputTaprootScriptPath(it, tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) } // one signature is not enough val tx = tmp.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubkey, scriptTree, ScriptWitness(listOf(sigs[0], sigs[0], sigs[0])), scriptTree)) @@ -262,7 +262,7 @@ class TaprootTestsCommon { lockTime = 0 ) // We still need to provide the tapscript tree because it is used to tweak the private key. - val sig = Crypto.signTaprootKeyPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree) + val sig = Transaction.signInputTaprootKeyPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, scriptTree) tmp.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) } @@ -287,7 +287,7 @@ class TaprootTestsCommon { txOut = listOf(TxOut(fundingTx1.txOut[0].amount - Satoshi(5000), sweepPublicKeyScript)), lockTime = 0 ) - val sig = Crypto.signTaprootScriptPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[0].hash()) + val sig = Transaction.signInputTaprootScriptPath(privs[0], tmp, 0, listOf(fundingTx.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[0].hash()) val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves[0], ScriptWitness(listOf(sig)), scriptTree) tmp.updateWitness(0, witness) } @@ -314,7 +314,7 @@ class TaprootTestsCommon { txOut = listOf(TxOut(fundingTx2.txOut[0].amount - Satoshi(5000), sweepPublicKeyScript)), lockTime = 0 ) - val sig = Crypto.signTaprootScriptPath(privs[1], tmp, 0, listOf(fundingTx2.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[1].hash()) + val sig = Transaction.signInputTaprootScriptPath(privs[1], tmp, 0, listOf(fundingTx2.txOut[0]), SigHash.SIGHASH_DEFAULT, leaves[1].hash()) val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves[1], ScriptWitness(listOf(sig)), scriptTree) tmp.updateWitness(0, witness) } @@ -340,7 +340,7 @@ class TaprootTestsCommon { txOut = listOf(TxOut(fundingTx3.txOut[0].amount - Satoshi(5000), addressToPublicKeyScript(blockchain, "tb1qxy9hhxkw7gt76qrm4yzw4j06gkk4evryh8ayp7").right!!)), lockTime = 0 ) - val sig = Crypto.signTaprootScriptPath(privs[2], tmp, 0, listOf(fundingTx3.txOut[1]), SigHash.SIGHASH_DEFAULT, leaves[2].hash()) + val sig = Transaction.signInputTaprootScriptPath(privs[2], tmp, 0, listOf(fundingTx3.txOut[1]), SigHash.SIGHASH_DEFAULT, leaves[2].hash()) val witness = Script.witnessScriptPathPay2tr(internalPubkey, leaves[2], ScriptWitness(listOf(sig)), scriptTree) tmp.updateWitness(0, witness) } @@ -410,7 +410,7 @@ class TaprootTestsCommon { fun `parse and validate large ordinals transaction`() { val file = resourcesDir().resolve("b5a7e05f28d00e4a791759ad7b6bd6799d856693293ceeaad9b0bb93c8851f7f.bin").openReadableFile() val buffer = ByteArray(file.size) - file.readBytes(buffer, 0, buffer.size) + file.readBytes(buffer, 0, buffer.size) file.close() val tx = Transaction.read(buffer) val parentTx = Transaction.read( diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt new file mode 100644 index 00000000..46b7778e --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2TestsCommon.kt @@ -0,0 +1,381 @@ +package fr.acinq.bitcoin.crypto.musig2 + +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.reference.TransactionTestsCommon +import fr.acinq.secp256k1.Hex +import kotlinx.serialization.json.* +import kotlin.random.Random +import kotlin.test.* + +class Musig2TestsCommon { + @Test + fun `aggregate public keys`() { + val tests = TransactionTestsCommon.readData("musig2/key_agg_vectors.json") + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = XonlyPublicKey(ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content)) + val (aggkey, _) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) + assertEquals(expected, aggkey) + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val tweakIndex = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int }.firstOrNull() + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + when (tweakIndex) { + null -> { + // One of the public keys is invalid, so key aggregation will fail. + // Callers must verify that public keys are valid before aggregating them. + assertFails { + KeyAggCache.create(keyIndices.map { pubkeys[it] }) + } + } + else -> { + // The tweak cannot be applied, it would result in an invalid public key. + val (_, cache) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) + assertTrue(cache.tweak(tweaks[tweakIndex], isXonly[0]).isLeft) + } + } + } + } + + /** Secret nonces in test vectors use a custom encoding. */ + private fun deserializeSecretNonce(hex: String): SecretNonce { + val serialized = Hex.decode(hex) + require(serialized.size == 97) { "secret nonce from test vector should be serialized using 97 bytes" } + // In test vectors, secret nonces are serialized as: + val compressedPublicKey = PublicKey.parse(serialized.takeLast(33).toByteArray()) + // We expect secret nonces serialized as: + // Where we use a different endianness for the public key coordinates than the test vectors. + val uncompressedPublicKey = compressedPublicKey.toUncompressedBin() + val publicKeyX = uncompressedPublicKey.drop(1).take(32).reversed().toByteArray() + val publicKeyY = uncompressedPublicKey.takeLast(32).reversed().toByteArray() + val magic = Hex.decode("220EDCF1") + return SecretNonce(magic + serialized.take(64) + publicKeyX + publicKeyY) + } + + @Test + fun `generate secret nonce`() { + val tests = TransactionTestsCommon.readData("musig2/nonce_gen_vectors.json") + tests.jsonObject["test_cases"]!!.jsonArray.forEach { + val randprime = ByteVector32.fromValidHex(it.jsonObject["rand_"]!!.jsonPrimitive.content) + val sk = it.jsonObject["sk"]?.jsonPrimitive?.contentOrNull?.let { PrivateKey.fromHex(it) } + val pk = PublicKey.fromHex(it.jsonObject["pk"]!!.jsonPrimitive.content) + val keyagg = it.jsonObject["aggpk"]?.jsonPrimitive?.contentOrNull?.let { + // The test vectors directly provide an aggregated public key: we must manually create the corresponding + // key aggregation cache to correctly test. + val agg = XonlyPublicKey(ByteVector32.fromValidHex(it)) + val magic = Hex.decode("f4adbbdf") + KeyAggCache(magic + agg.publicKey.toUncompressedBin().drop(1) + ByteArray(129) { 0x00 }) + } + val msg = it.jsonObject["msg"]?.jsonPrimitive?.contentOrNull?.let { Hex.decode(it) } + val extraInput = it.jsonObject["extra_in"]?.jsonPrimitive?.contentOrNull?.let { Hex.decode(it) } + val expectedSecnonce = deserializeSecretNonce(it.jsonObject["expected_secnonce"]!!.jsonPrimitive.content) + val expectedPubnonce = IndividualNonce(it.jsonObject["expected_pubnonce"]!!.jsonPrimitive.content) + // secp256k1 only supports signing 32-byte messages (when provided), which excludes some of the test vectors. + if (msg == null || msg.size == 32) { + val (secnonce, pubnonce) = SecretNonce.generate(randprime, sk, pk, msg?.byteVector32(), keyagg, extraInput?.byteVector32()) + assertEquals(expectedPubnonce, pubnonce) + assertEquals(expectedSecnonce, secnonce) + } + } + } + + @Test + fun `aggregate nonces`() { + val tests = TransactionTestsCommon.readData("musig2/nonce_agg_vectors.json") + val nonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val nonceIndices = it.jsonObject["pnonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = AggregatedNonce(it.jsonObject["expected"]!!.jsonPrimitive.content) + val agg = IndividualNonce.aggregate(nonceIndices.map { nonces[it] }).right + assertNotNull(agg) + assertEquals(expected, agg) + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val nonceIndices = it.jsonObject["pnonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + assertTrue(IndividualNonce.aggregate(nonceIndices.map { nonces[it] }).isLeft) + } + } + + @Test + fun sign() { + val tests = TransactionTestsCommon.readData("musig2/sign_verify_vectors.json") + val sk = PrivateKey.fromHex(tests.jsonObject["sk"]!!.jsonPrimitive.content) + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val secnonces = tests.jsonObject["secnonces"]!!.jsonArray.map { deserializeSecretNonce(it.jsonPrimitive.content) } + val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + val aggnonces = tests.jsonObject["aggnonces"]!!.jsonArray.map { AggregatedNonce(it.jsonPrimitive.content) } + val msgs = tests.jsonObject["msgs"]!!.jsonArray.map { ByteVector(it.jsonPrimitive.content) } + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) + val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int + val messageIndex = it.jsonObject["msg_index"]!!.jsonPrimitive.int + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) + assertEquals(aggnonces[it.jsonObject["aggnonce_index"]!!.jsonPrimitive.int], aggnonce) + val keyagg = KeyAggCache.create(keyIndices.map { pubkeys[it] }).second + // We only support signing 32-byte messages. + if (msgs[messageIndex].bytes.size == 32) { + val session = Session.create(aggnonce, ByteVector32(msgs[messageIndex]), keyagg) + assertNotNull(session) + val psig = session.sign(secnonces[keyIndices[signerIndex]], sk) + assertEquals(expected, psig) + assertTrue(session.verify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]])) + } + } + tests.jsonObject["verify_fail_test_cases"]!!.jsonArray.forEach { + val psig = Hex.decode(it.jsonObject["sig"]!!.jsonPrimitive.content).byteVector32() + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int + val messageIndex = it.jsonObject["msg_index"]!!.jsonPrimitive.int + if (msgs[messageIndex].bytes.size == 32) { + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) + val (_, keyagg) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) + val session = Session.create(aggnonce, ByteVector32(msgs[messageIndex]), keyagg) + assertNotNull(session) + assertFalse(session.verify(psig, pnonces[signerIndex], pubkeys[signerIndex])) + } + } + } + + @Test + fun `aggregate signatures`() { + val tests = TransactionTestsCommon.readData("musig2/sig_agg_vectors.json") + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + val psigs = tests.jsonObject["psigs"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + val msg = ByteVector32.fromValidHex(tests.jsonObject["msg"]!!.jsonPrimitive.content) + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val psigIndices = it.jsonObject["psig_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = ByteVector64.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) + val keyagg = tweakIndices + .zip(isXonly) + .map { tweaks[it.first] to it.second } + .fold(KeyAggCache.create(keyIndices.map { pubkeys[it] }).second) { agg, (tweak, isXonly) -> agg.tweak(tweak, isXonly).right!!.first } + val session = Session.create(aggnonce, msg, keyagg) + val aggsig = session.aggregateSigs(psigIndices.map { psigs[it] }).right + assertEquals(expected, aggsig) + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val psigIndices = it.jsonObject["psig_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val aggnonce = IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right + assertNotNull(aggnonce) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + assertEquals(AggregatedNonce(it.jsonObject["aggnonce"]!!.jsonPrimitive.content), aggnonce) + val keyagg = tweakIndices + .zip(isXonly) + .map { tweaks[it.first] to it.second } + .fold(KeyAggCache.create(keyIndices.map { pubkeys[it] }).second) { agg, (tweak, isXonly) -> agg.tweak(tweak, isXonly).right!!.first } + val session = Session.create(aggnonce, msg, keyagg) + assertTrue(session.aggregateSigs(psigIndices.map { psigs[it] }).isLeft) + } + } + + @Test + fun `tweak tests`() { + val tests = TransactionTestsCommon.readData("musig2/tweak_vectors.json") + val sk = PrivateKey.fromHex(tests.jsonObject["sk"]!!.jsonPrimitive.content) + val pubkeys = tests.jsonObject["pubkeys"]!!.jsonArray.map { PublicKey(ByteVector(it.jsonPrimitive.content)) } + val pnonces = tests.jsonObject["pnonces"]!!.jsonArray.map { IndividualNonce(it.jsonPrimitive.content) } + val tweaks = tests.jsonObject["tweaks"]!!.jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) } + val msg = ByteVector32.fromValidHex(tests.jsonObject["msg"]!!.jsonPrimitive.content) + + val secnonce = deserializeSecretNonce(tests.jsonObject["secnonce"]!!.jsonPrimitive.content) + val aggnonce = AggregatedNonce(tests.jsonObject["aggnonce"]!!.jsonPrimitive.content) + + assertEquals(pubkeys[0], sk.publicKey()) + assertEquals(aggnonce, IndividualNonce.aggregate(listOf(pnonces[0], pnonces[1], pnonces[2])).right) + + tests.jsonObject["valid_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val expected = ByteVector32.fromValidHex(it.jsonObject["expected"]!!.jsonPrimitive.content) + assertEquals(aggnonce, IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean } + val signerIndex = it.jsonObject["signer_index"]!!.jsonPrimitive.int + val keyagg = tweakIndices.fold(KeyAggCache.create(keyIndices.map { pubkeys[it] }).second) { keyAgg, tweakIdx -> keyAgg.tweak(tweaks[tweakIdx], isXonly[tweakIdx]).right!!.first } + val session = Session.create(aggnonce, msg, keyagg) + assertNotNull(session) + val psig = session.sign(secnonce, sk) + assertEquals(expected, psig) + assertTrue(session.verify(psig, pnonces[nonceIndices[signerIndex]], pubkeys[keyIndices[signerIndex]])) + } + tests.jsonObject["error_test_cases"]!!.jsonArray.forEach { + val keyIndices = it.jsonObject["key_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + val nonceIndices = it.jsonObject["nonce_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + assertEquals(aggnonce, IndividualNonce.aggregate(nonceIndices.map { pnonces[it] }).right) + val tweakIndices = it.jsonObject["tweak_indices"]!!.jsonArray.map { it.jsonPrimitive.int } + assertEquals(1, tweakIndices.size) + val tweak = tweaks[tweakIndices.first()] + val isXonly = it.jsonObject["is_xonly"]!!.jsonArray.map { it.jsonPrimitive.boolean }.first() + val (_, keyagg) = KeyAggCache.create(keyIndices.map { pubkeys[it] }) + assertTrue(keyagg.tweak(tweak, isXonly).isLeft) + } + } + + @Test + fun `simple musig2 example`() { + val msg = Random.Default.nextBytes(32).byteVector32() + val privkeys = listOf( + PrivateKey(ByteArray(32) { 1 }), + PrivateKey(ByteArray(32) { 2 }), + PrivateKey(ByteArray(32) { 3 }), + ) + val pubkeys = privkeys.map { it.publicKey() } + + val plainTweak = ByteVector32("this could be a BIP32 tweak....".encodeToByteArray() + ByteArray(1)) + val xonlyTweak = ByteVector32("this could be a taproot tweak..".encodeToByteArray() + ByteArray(1)) + + // Aggregate public keys from all participants, and apply tweaks. + val (keyAggCache, aggpub) = run { + val (_, c) = KeyAggCache.create(pubkeys) + val (c1, _) = c.tweak(plainTweak, false).right!! + c1.tweak(xonlyTweak, true).right!! + } + + // Generate secret nonces for each participant. + val nonces = privkeys.map { SecretNonce.generate(Random.Default.nextBytes(32).byteVector32(), it, it.publicKey(), message = null, keyAggCache, extraInput = null) } + val secnonces = nonces.map { it.first } + val pubnonces = nonces.map { it.second } + + // Aggregate public nonces. + val aggnonce = IndividualNonce.aggregate(pubnonces).right + assertNotNull(aggnonce) + + // Create partial signatures from each participant. + val session = Session.create(aggnonce, msg, keyAggCache) + val psigs = privkeys.indices.map { session.sign(secnonces[it], privkeys[it]) } + // Verify individual partial signatures. + pubkeys.indices.forEach { assertTrue(session.verify(psigs[it], pubnonces[it], pubkeys[it])) } + // Aggregate partial signatures into a single signature. + val aggsig = session.aggregateSigs(psigs).right + assertNotNull(aggsig) + // Check that the aggregated signature is a valid, plain Schnorr signature for the aggregated public key. + assertTrue(Crypto.verifySignatureSchnorr(msg, aggsig, aggpub.xOnly())) + } + + @Test + fun `use musig2 to replace multisig 2-of-2`() { + val alicePrivKey = PrivateKey(ByteArray(32) { 1 }) + val alicePubKey = alicePrivKey.publicKey() + val bobPrivKey = PrivateKey(ByteArray(32) { 2 }) + val bobPubKey = bobPrivKey.publicKey() + + // Alice and Bob exchange public keys and agree on a common aggregated key. + val internalPubKey = Musig2.aggregateKeys(listOf(alicePubKey, bobPubKey)) + val commonPubKey = internalPubKey.outputKey(Crypto.TaprootTweak.NoScriptTweak).first + + // This tx sends to a taproot script that doesn't contain any script path. + val tx = Transaction(2, listOf(), listOf(TxOut(10_000.sat(), Script.pay2tr(commonPubKey))), 0) + // This tx spends the previous tx with Alice and Bob's signatures. + val spendingTx = Transaction(2, listOf(TxIn(OutPoint(tx, 0), sequence = 0)), listOf(TxOut(10_000.sat(), Script.pay2wpkh(alicePubKey))), 0) + + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val aliceNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), alicePrivKey, listOf(alicePubKey, bobPubKey)) + val bobNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), bobPrivKey, listOf(alicePubKey, bobPubKey)) + + // Once they have each other's public nonce, they can produce partial signatures. + val publicNonces = listOf(aliceNonce.second, bobNonce.second) + val aliceSig = Musig2.signTaprootInput(alicePrivKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), aliceNonce.first, publicNonces, scriptTree = null).right + assertNotNull(aliceSig) + val bobSig = Musig2.signTaprootInput(bobPrivKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), bobNonce.first, publicNonces, scriptTree = null).right + assertNotNull(bobSig) + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val aggregateSig = Musig2.aggregateTaprootSignatures(listOf(aliceSig, bobSig), spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), publicNonces, scriptTree = null).right + assertNotNull(aggregateSig) + + // This tx looks like any other tx that spends a p2tr output, with a single signature. + val signedSpendingTx = spendingTx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregateSig)) + Transaction.correctlySpends(signedSpendingTx, tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + @Test + fun `swap-in-potentiam example with musig2 and taproot`() { + val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) + val userPublicKey = userPrivateKey.publicKey() + val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) + val serverPublicKey = serverPrivateKey.publicKey() + val userRefundPrivateKey = PrivateKey(ByteArray(32) { 3 }) + val refundDelay = 25920 + + // The redeem script is just the refund script, generated from this policy: and_v(v:pk(user),older(refundDelay)) + // It does not depend upon the user's or server's key, just the user's refund key and the refund delay. + val redeemScript = listOf(OP_PUSHDATA(userRefundPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + val scriptTree = ScriptTree.Leaf(0, redeemScript) + + // The internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key. + val internalPubKey = Musig2.aggregateKeys(listOf(userPublicKey, serverPublicKey)) + // It is tweaked with the script's merkle root to get the pubkey that will be exposed. + val pubkeyScript = Script.pay2tr(internalPubKey, scriptTree) + + val swapInTx = Transaction( + version = 2, + txIn = listOf(), + txOut = listOf(TxOut(10_000.sat(), pubkeyScript)), + lockTime = 0 + ) + + // The transaction can be spent if the user and the server produce a signature. + run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(10_000.sat(), Script.pay2wpkh(userPublicKey))), + lockTime = 0 + ) + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val userNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), userPrivateKey, listOf(userPublicKey, serverPublicKey)) + val serverNonce = Musig2.generateNonce(Random.Default.nextBytes(32).byteVector32(), serverPrivateKey, listOf(userPublicKey, serverPublicKey)) + + // Once they have each other's public nonce, they can produce partial signatures. + val publicNonces = listOf(userNonce.second, serverNonce.second) + val userSig = Musig2.signTaprootInput(userPrivateKey, tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), userNonce.first, publicNonces, scriptTree).right + assertNotNull(userSig) + val serverSig = Musig2.signTaprootInput(serverPrivateKey, tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), serverNonce.first, publicNonces, scriptTree).right + assertNotNull(serverSig) + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val aggregateSig = Musig2.aggregateTaprootSignatures(listOf(userSig, serverSig), tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), publicNonces, scriptTree).right + assertNotNull(aggregateSig) + val signedTx = tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregateSig)) + Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + // Or it can be spent with only the user's signature, after a delay. + run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())), + txOut = listOf(TxOut(10_000.sat(), Script.pay2wpkh(userPublicKey))), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, scriptTree.hash()) + val signedTx = tx.updateWitness(0, Script.witnessScriptPathPay2tr(internalPubKey, scriptTree, ScriptWitness(listOf(sig)), scriptTree)) + Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + } +} \ No newline at end of file