Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {
val currentOs = org.gradle.internal.os.OperatingSystem.current()

group = "fr.acinq.bitcoin"
version = "0.22.2"
version = "0.22.3-SNAPSHOT"

repositories {
google()
Expand Down
52 changes: 51 additions & 1 deletion src/commonMain/kotlin/fr/acinq/bitcoin/crypto/musig2/Musig2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ 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.bitcoin.utils.getOrElse
import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic

/**
Expand Down Expand Up @@ -151,6 +151,26 @@ public data class SecretNonce(internal val data: ByteVector) {
val publicNonce = IndividualNonce(nonce.copyOfRange(Secp256k1.MUSIG2_SECRET_NONCE_SIZE, Secp256k1.MUSIG2_SECRET_NONCE_SIZE + Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE))
return Pair(secretNonce, publicNonce)
}

/**
* Alternative counter-based method for generating nonce.
* 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 nonRepeatingCounter non-repeating counter that must never be reused with the same private key.
* @param privateKey signer's private 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 generateWithCounter(nonRepeatingCounter: ULong, privateKey: PrivateKey, message: ByteVector32?, keyAggCache: KeyAggCache?, extraInput: ByteVector32?): Pair<SecretNonce, IndividualNonce> {
val nonce = Secp256k1.musigNonceGenCounter(nonRepeatingCounter, privateKey.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)
}
}
}

Expand Down Expand Up @@ -279,6 +299,36 @@ public object Musig2 {
return taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree).map { it.sign(secretNonce, privateKey) }
}

/**
* Verify a partial musig2 signature.

* @param partialSig partial musig2 signature.
* @param nonce public nonce matching the secret nonce used to generate the signature.
* @param publicKey public key for the private key used to generate the signature.
* @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.
* @return true if the partial signature is valid.
*/
@JvmStatic
public fun verify(
partialSig: ByteVector32,
nonce: IndividualNonce,
publicKey: PublicKey,
tx: Transaction,
inputIndex: Int,
inputs: List<TxOut>,
publicKeys: List<PublicKey>,
publicNonces: List<IndividualNonce>,
scriptTree: ScriptTree?
): Boolean {
val session = taprootSession(tx, inputIndex, inputs, publicKeys, publicNonces, scriptTree)
return session.map { it.verify(partialSig, nonce, publicKey) }.getOrElse { false }
}

/**
* Aggregate partial musig2 signatures into a valid schnorr signature for the given taproot input key path.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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
Expand Down Expand Up @@ -83,6 +82,33 @@ class Musig2TestsCommon {
}
}

@Test
fun `generate secret nonce from counter`() {
val privateKey = PrivateKey.fromHex("EEC1CB7D1B7254C5CAB0D9C61AB02E643D464A59FE6C96A7EFE871F07C5AEF54")
run {
val nonce = SecretNonce.generateWithCounter(0UL, privateKey, null, null, null)
assertEquals(ByteVector.fromHex("03A5B9B6907942EACDDA49A366016EC2E62404A1BF4AB6D4DB82067BC3ADF086D7033205DB9EB34D5C7CE02848CAC68A83ED73E3883477F563F23CE9A11A7721EC64"), nonce.second.data)

val nonce1 = SecretNonce.generateWithCounter(0UL, privateKey, null, null, null)
assertEquals(nonce, nonce1)

val nonce2 = SecretNonce.generateWithCounter(0UL, PrivateKey.fromHex("EEC1CB7D1B7254C5CAB0D9C61AB02E643D464A59FE6C96A7EFE871F07C5AEF55"), null, null, null)
assertNotEquals(nonce, nonce2)
}
run {
val nonce = SecretNonce.generateWithCounter(0UL, privateKey, ByteVector32.fromValidHex("380CD17A198FC3DAD3B7DA7492941F46976F2702FF7C66F24F472036AF1DA3F9"), null, null)
assertEquals(ByteVector.fromHex("0390B0553BA461A5BAC3F72BB86338D5FE8BB833ED7A21D3E21498C068D9A6E802020D641B37264FD22AC5E2F9FB868BAB49EB02FCB81AEC247FFD057DE37E1CB173"), nonce.second.data)
}
run {
val nonce = SecretNonce.generateWithCounter(1UL, privateKey, null, null, null)
assertEquals(ByteVector.fromHex("0340A08273BBC9ED0A2BFBDBDAFCCB43073865643593988841F67E665864767047037844A24EC0B763CE73F8252445DDDDFB7CD10498D796AD7217B841882A3A9961"), nonce.second.data)
}
run {
val nonce = SecretNonce.generateWithCounter(1UL, privateKey, null, null, null)
assertEquals(ByteVector.fromHex("0340A08273BBC9ED0A2BFBDBDAFCCB43073865643593988841F67E665864767047037844A24EC0B763CE73F8252445DDDDFB7CD10498D796AD7217B841882A3A9961"), nonce.second.data)
}
}

@Test
fun `aggregate nonces`() {
val tests = TestHelpers.readResourceAsJson("musig2/nonce_agg_vectors.json")
Expand Down Expand Up @@ -298,10 +324,15 @@ class Musig2TestsCommon {

// 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)
assertTrue(Musig2.verify(aliceSig, aliceNonce.second, alicePubKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), publicNonces, scriptTree = null))


val bobSig = Musig2.signTaprootInput(bobPrivKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), bobNonce.first, publicNonces, scriptTree = null).right
assertNotNull(bobSig)
assertTrue(Musig2.verify(bobSig, bobNonce.second, bobPubKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), publicNonces, scriptTree = null))

// 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
Expand All @@ -312,6 +343,41 @@ class Musig2TestsCommon {
Transaction.correctlySpends(signedSpendingTx, tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}

@Test
fun `verify musig2 signatures`() {
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

val tx = Transaction(2, listOf(), listOf(TxOut(10_000.sat(), Script.pay2tr(commonPubKey))), 0)
val spendingTx = Transaction(2, listOf(TxIn(OutPoint(tx, 0), sequence = 0)), listOf(TxOut(10_000.sat(), Script.pay2wpkh(alicePubKey))), 0)

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))
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
require(aliceSig != null)
assertTrue(Musig2.verify(aliceSig, aliceNonce.second, alicePubKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), publicNonces, scriptTree = null))

// wrong signature
assertFalse(Musig2.verify(aliceSig.reversed(), aliceNonce.second, alicePubKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), publicNonces, scriptTree = null))

// wrong public key
assertFalse(Musig2.verify(aliceSig, aliceNonce.second, bobPubKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), publicNonces, scriptTree = null))

// wrong nonce
assertFalse(Musig2.verify(aliceSig, aliceNonce.second, alicePubKey, spendingTx, 0, listOf(tx.txOut[0]), listOf(alicePubKey, bobPubKey), listOf(aliceNonce.second, aliceNonce.second), scriptTree = null))

// wrong inputs
assertFalse(Musig2.verify(aliceSig, aliceNonce.second, alicePubKey, spendingTx, 0, listOf(tx.txOut[0], tx.txOut[0]), listOf(alicePubKey, bobPubKey), listOf(aliceNonce.second, aliceNonce.second), scriptTree = null))
}

@Test
fun `swap-in-potentiam example with musig2 and taproot`() {
val userPrivateKey = PrivateKey(ByteArray(32) { 1 })
Expand Down Expand Up @@ -355,8 +421,11 @@ class Musig2TestsCommon {
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)
assertTrue(Musig2.verify(userSig, userNonce.second, userPublicKey, tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), publicNonces, scriptTree))

val serverSig = Musig2.signTaprootInput(serverPrivateKey, tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), serverNonce.first, publicNonces, scriptTree).right
assertNotNull(serverSig)
assertTrue(Musig2.verify(serverSig, serverNonce.second, serverPublicKey, tx, 0, swapInTx.txOut, listOf(userPublicKey, serverPublicKey), publicNonces, scriptTree))

// 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
Expand Down