Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add high-level types for musig2 primitives provided by secp256k1-kmp #107

Merged
merged 7 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we merge with that temporary version since we don't expect to do any other work on bitcoin-kmp that would require a quick release? And then once we can merge ACINQ/secp256k1-kmp#93 we'll be able to make clean releases and fix the versions?


repositories {
google()
Expand Down Expand Up @@ -45,7 +45,7 @@ kotlin {
}

sourceSets {
val secp256k1KmpVersion = "0.13.0"
val secp256k1KmpVersion = "0.14.0"

val commonMain by getting {
dependencies {
Expand Down
18 changes: 0 additions & 18 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Crypto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<TxOut>, 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<TxOut>, 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())
Expand Down
47 changes: 47 additions & 0 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<TxOut>, 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<TxOut>,
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<OutPoint, TxOut>, scriptFlags: Int) {
val prevouts = tx.txIn.map { previousOutputs[it.outPoint]!! }
Expand Down
295 changes: 295 additions & 0 deletions src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt
Original file line number Diff line number Diff line change
@@ -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<Throwable, Pair<KeyAggCache, PublicKey>> = 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<PublicKey>): Pair<XonlyPublicKey, KeyAggCache> {
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<ByteVector32>): Either<Throwable, ByteVector64> = 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 = "<secret_nonce>"

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<SecretNonce, IndividualNonce> {
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<IndividualNonce>): Either<Throwable, AggregatedNonce> = 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<PublicKey>): 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<PublicKey>): Pair<SecretNonce, IndividualNonce> {
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<TxOut>, publicKeys: List<PublicKey>, publicNonces: List<IndividualNonce>, scriptTree: ScriptTree?): Either<Throwable, Session> {
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<TxOut>,
publicKeys: List<PublicKey>,
secretNonce: SecretNonce,
publicNonces: List<IndividualNonce>,
scriptTree: ScriptTree?
): Either<Throwable, ByteVector32> {
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<ByteVector32>,
tx: Transaction,
inputIndex: Int,
inputs: List<TxOut>,
publicKeys: List<PublicKey>,
publicNonces: List<IndividualNonce>,
scriptTree: ScriptTree?
): Either<Throwable, ByteVector64> {
return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).flatMap { it.aggregateSigs(partialSigs) }
}

}
Loading
Loading