mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 01:40:55 +01:00
Musig2 Implementation (#4418)
* An initial (not yet working) implementation with test * Added custom (non-bip-340) verification for now * Made KeySet a case class * Got MuSig2 working with BIP340 verification passing * Responds to Ben's review * Fixed hash tags and added parital signature verification * Added point multiplication that allows infinity and did some refactoring * Refactored type defs into case classes * Added tests for more signers and fixed single-party bug * Added key aggregation test vectors from BIP * Added nonce generation test vectors from BIP * Added nonce aggregation test vectors from BIP * Made nonce aggregation test vectors pass by having MultiNoncePub wrap SecpPoints * Added remaining static test vectors from BIP * Implements tweaking support and adds tests, including all of the remaining BIP tests * Added factory objects for nonce types * Refactored things into multiple files with renaming and restructuring * Some minor renaming * Introduced ParityMultiplier ADT to remove unneccesary computations * Added scaladocs * Added messages to invariants * Fixed a typo * Nonce generation now takes a SchnorrPublicKey instead of raw bytes * Made point multiplication more robust * Responded to Ben nits * Added musig.md
This commit is contained in:
parent
eae16a52f8
commit
ae0962d7ed
@ -0,0 +1,628 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto.musig.MuSigUtil._
|
||||
import org.bitcoins.crypto._
|
||||
import org.scalacheck.Gen
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
class MuSigTest extends BitcoinSCryptoTest {
|
||||
behavior of "MuSig2 Implementation"
|
||||
|
||||
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
|
||||
generatorDrivenConfigNewCode
|
||||
|
||||
it should "work for a single party" in {
|
||||
forAll(CryptoGenerators.privateKey, NumberGenerator.bytevector(32)) {
|
||||
case (privKey, msg) =>
|
||||
val pubKey = privKey.publicKey
|
||||
val noncePriv: MuSigNoncePriv = MuSigNoncePriv.gen()
|
||||
val noncePub: MuSigNoncePub = noncePriv.toPublicNonces
|
||||
val keySet = KeySet(pubKey.schnorrPublicKey)
|
||||
val aggMuSigNoncePub = MuSigNoncePub.aggregate(Vector(noncePub))
|
||||
|
||||
assert(aggMuSigNoncePub == noncePub)
|
||||
|
||||
val (aggNonce, s) =
|
||||
sign(noncePriv, aggMuSigNoncePub, privKey, msg, keySet)
|
||||
|
||||
assert(
|
||||
partialSigVerify(s,
|
||||
noncePub,
|
||||
aggMuSigNoncePub,
|
||||
pubKey.schnorrPublicKey,
|
||||
keySet,
|
||||
msg))
|
||||
|
||||
val sig = signAgg(Vector(s), aggNonce)
|
||||
|
||||
assert(sig == SchnorrDigitalSignature(aggNonce.schnorrNonce, s))
|
||||
|
||||
val aggPub = keySet.aggPubKey
|
||||
|
||||
assert(
|
||||
aggPub.schnorrPublicKey == pubKey
|
||||
.multiply(keySet.keyAggCoef(pubKey.schnorrPublicKey))
|
||||
.schnorrPublicKey)
|
||||
|
||||
assert(aggPub.schnorrPublicKey.verify(msg, sig))
|
||||
}
|
||||
}
|
||||
|
||||
it should "work for two parties" in {
|
||||
forAll(CryptoGenerators.privateKey,
|
||||
CryptoGenerators.privateKey,
|
||||
NumberGenerator.bytevector(32)) { case (priv1, priv2, msg) =>
|
||||
val pub1 = priv1.publicKey
|
||||
val noncePriv1: MuSigNoncePriv = MuSigNoncePriv.gen()
|
||||
val noncePub1: MuSigNoncePub = noncePriv1.toPublicNonces
|
||||
val pub2 = priv2.publicKey
|
||||
val noncePriv2: MuSigNoncePriv = MuSigNoncePriv.gen()
|
||||
val noncePub2: MuSigNoncePub = noncePriv2.toPublicNonces
|
||||
val keySet: KeySet = KeySet(pub1.schnorrPublicKey, pub2.schnorrPublicKey)
|
||||
val aggMuSigNoncePub =
|
||||
MuSigNoncePub.aggregate(Vector(noncePub1, noncePub2))
|
||||
val (aggNonce1, s1) =
|
||||
sign(noncePriv1, aggMuSigNoncePub, priv1, msg, keySet)
|
||||
val (aggNonce2, s2) =
|
||||
sign(noncePriv2, aggMuSigNoncePub, priv2, msg, keySet)
|
||||
|
||||
assert(aggNonce1 == aggNonce2)
|
||||
assert(
|
||||
partialSigVerify(s1,
|
||||
noncePub1,
|
||||
aggMuSigNoncePub,
|
||||
pub1.schnorrPublicKey,
|
||||
keySet,
|
||||
msg))
|
||||
assert(
|
||||
partialSigVerify(s2,
|
||||
noncePub2,
|
||||
aggMuSigNoncePub,
|
||||
pub2.schnorrPublicKey,
|
||||
keySet,
|
||||
msg))
|
||||
|
||||
val sig = signAgg(Vector(s1, s2), aggNonce1)
|
||||
val aggPub = keySet.aggPubKey
|
||||
|
||||
assert(aggPub.schnorrPublicKey.verify(msg, sig))
|
||||
}
|
||||
}
|
||||
|
||||
it should "work for more parties" in {
|
||||
val privKeysGen: Gen[Vector[ECPrivateKey]] = Gen
|
||||
.choose[Int](2, 20)
|
||||
.flatMap(n => Gen.listOfN(n, CryptoGenerators.privateKey))
|
||||
.map(_.toVector)
|
||||
|
||||
forAll(
|
||||
privKeysGen,
|
||||
NumberGenerator.bytevector(32)
|
||||
) { case (privKeysUnsorted, msg) =>
|
||||
val keySet: KeySet = KeySet(privKeysUnsorted.map(_.schnorrPublicKey))
|
||||
val privKeys = keySet.keys.map(pubKey =>
|
||||
privKeysUnsorted.find(_.schnorrPublicKey == pubKey).get)
|
||||
val noncePrivs = privKeys.map(_ => MuSigNoncePriv.gen())
|
||||
val noncePubs = noncePrivs.map(_.toPublicNonces)
|
||||
val aggMuSigNoncePub = MuSigNoncePub.aggregate(noncePubs)
|
||||
val partialSigs: Vector[(ECPublicKey, FieldElement)] =
|
||||
privKeys.zipWithIndex.map { case (privKey, i) =>
|
||||
sign(noncePrivs(i), aggMuSigNoncePub, privKey, msg, keySet)
|
||||
}
|
||||
|
||||
// All aggregate nonces are the same
|
||||
assert(partialSigs.map(_._1).forall(_ == partialSigs.head._1))
|
||||
// All partial sigs are valid
|
||||
assert(partialSigs.map(_._2).zipWithIndex.forall { case (s, i) =>
|
||||
partialSigVerify(s,
|
||||
noncePubs(i),
|
||||
aggMuSigNoncePub,
|
||||
privKeys(i).schnorrPublicKey,
|
||||
keySet,
|
||||
msg)
|
||||
})
|
||||
|
||||
val sig = signAgg(partialSigs.map(_._2), partialSigs.head._1)
|
||||
val aggPub = keySet.aggPubKey
|
||||
|
||||
assert(aggPub.schnorrPublicKey.verify(msg, sig))
|
||||
}
|
||||
}
|
||||
|
||||
it should "work with tweaks" in {
|
||||
val privKeysGen: Gen[Vector[ECPrivateKey]] = Gen
|
||||
.choose[Int](2, 20)
|
||||
.flatMap(n => Gen.listOfN(n, CryptoGenerators.privateKey))
|
||||
.map(_.toVector)
|
||||
|
||||
val tweaksGen: Gen[Vector[MuSigTweak]] = Gen
|
||||
.choose[Int](0, 10)
|
||||
.flatMap(n =>
|
||||
Gen.listOfN(n,
|
||||
CryptoGenerators.fieldElement.flatMap(x =>
|
||||
NumberGenerator.bool.map((x, _)))))
|
||||
.map(_.toVector)
|
||||
.map(_.map { case (x, b) => MuSigTweak(x, b) })
|
||||
|
||||
forAll(
|
||||
privKeysGen,
|
||||
NumberGenerator.bytevector(32),
|
||||
tweaksGen
|
||||
) { case (privKeysUnsorted, msg, tweaks) =>
|
||||
val keySet: KeySet =
|
||||
KeySet(privKeysUnsorted.map(_.schnorrPublicKey), tweaks)
|
||||
val privKeys = keySet.keys.map(pubKey =>
|
||||
privKeysUnsorted.find(_.schnorrPublicKey == pubKey).get)
|
||||
val noncePrivs = privKeys.map(_ => MuSigNoncePriv.gen())
|
||||
val noncePubs = noncePrivs.map(_.toPublicNonces)
|
||||
val aggMuSigNoncePub = MuSigNoncePub.aggregate(noncePubs)
|
||||
val partialSigs: Vector[(ECPublicKey, FieldElement)] =
|
||||
privKeys.zipWithIndex.map { case (privKey, i) =>
|
||||
sign(noncePrivs(i), aggMuSigNoncePub, privKey, msg, keySet)
|
||||
}
|
||||
|
||||
// All aggregate nonces are the same
|
||||
assert(partialSigs.map(_._1).forall(_ == partialSigs.head._1))
|
||||
// All partial sigs are valid
|
||||
assert(partialSigs.map(_._2).zipWithIndex.forall { case (s, i) =>
|
||||
partialSigVerify(s,
|
||||
noncePubs(i),
|
||||
aggMuSigNoncePub,
|
||||
privKeys(i).schnorrPublicKey,
|
||||
keySet,
|
||||
msg)
|
||||
})
|
||||
|
||||
val sig = signAgg(partialSigs.map(_._2), aggMuSigNoncePub, keySet, msg)
|
||||
val aggPub = keySet.aggPubKey
|
||||
|
||||
assert(aggPub.schnorrPublicKey.verify(msg, sig))
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/jonasnick/bips/blob/263a765a77e20efe883ed3b28dc155a0d8c7d61a/bip-musig2/reference.py#L391
|
||||
it should "pass key aggregation test vectors" in {
|
||||
val inputs = Vector(
|
||||
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
|
||||
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
|
||||
"3590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66"
|
||||
).map(SchnorrPublicKey.fromHex)
|
||||
|
||||
val expected =
|
||||
Vector(
|
||||
"E5830140512195D74C8307E39637CBE5FB730EBEAB80EC514CF88A877CEEEE0B",
|
||||
"D70CD69A2647F7390973DF48CBFA2CCC407B8B2D60B08C5F1641185C7998A290",
|
||||
"81A8B093912C9E481408D09776CEFB48AEB8B65481B6BAAFB3C5810106717BEB",
|
||||
"2EB18851887E7BDC5E830E89B19DDBC28078F1FA88AAD0AD01CA06FE4F80210B"
|
||||
).map(SchnorrPublicKey.fromHex)
|
||||
|
||||
// Vector 1
|
||||
assert(UnsortedKeySet(inputs).aggPubKey.schnorrPublicKey == expected(0))
|
||||
// Vector 2
|
||||
assert(
|
||||
UnsortedKeySet(inputs.reverse).aggPubKey.schnorrPublicKey == expected(1))
|
||||
// Vector 3
|
||||
assert(
|
||||
UnsortedKeySet(
|
||||
Vector.fill(3)(inputs(0))).aggPubKey.schnorrPublicKey == expected(2))
|
||||
// Vector 4
|
||||
assert(
|
||||
UnsortedKeySet(
|
||||
Vector(inputs(0),
|
||||
inputs(0),
|
||||
inputs(1),
|
||||
inputs(1))).aggPubKey.schnorrPublicKey == expected(3))
|
||||
|
||||
// The following errors must be handled by the caller as we can't even represent them
|
||||
// Vector 5
|
||||
assertThrows[IllegalArgumentException](
|
||||
SchnorrPublicKey.fromHex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000005"))
|
||||
// Vector 6
|
||||
assertThrows[IllegalArgumentException](
|
||||
SchnorrPublicKey.fromHex(
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"))
|
||||
// Vector 7
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigTweak(
|
||||
FieldElement(
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"),
|
||||
isXOnlyT = true))
|
||||
// Vector 8
|
||||
val schnorrG = CryptoParams.getG.schnorrPublicKey
|
||||
val keySetNoTweak = KeySet(schnorrG)
|
||||
val coeff = keySetNoTweak.keyAggCoef(schnorrG).negate
|
||||
val keySet =
|
||||
keySetNoTweak.withTweaks(Vector(MuSigTweak(coeff, isXOnlyT = false)))
|
||||
assertThrows[Exception](keySet.aggPubKey)
|
||||
}
|
||||
|
||||
// https://github.com/jonasnick/bips/blob/263a765a77e20efe883ed3b28dc155a0d8c7d61a/bip-musig2/reference.py#L436
|
||||
it should "pass nonce generation test vectors" in {
|
||||
val rand = ByteVector.fill(32)(0)
|
||||
val msg = Some(ByteVector.fill(32)(1))
|
||||
val sk = Some(ECPrivateKey(ByteVector.fill(32)(2)))
|
||||
val aggpk = Some(SchnorrPublicKey(ByteVector.fill(32)(7)))
|
||||
val extraIn = Some(ByteVector.fill(32)(8))
|
||||
|
||||
val expected = Vector(
|
||||
"E8F2E103D86800F19A4E97338D371CB885DB2F19D08C0BD205BBA9B906C971D0" ++
|
||||
"D786A17718AAFAD6DE025DDDD99DC823E2DFC1AE1DDFE920888AD53FFF423FC4",
|
||||
"8A633F5EECBDB690A6BE4921426F41BE78D509DC1CE894C1215844C0E4C6DE7A" ++
|
||||
"BC9A5BE0A3BF3FE312CCB7E4817D2CB17A7CEA8382B73A99A583E323387B3C32",
|
||||
"7B3B5A002356471AF0E961DE2549C121BD0D48ABCEEDC6E034BDDF86AD3E0A18" ++
|
||||
"7ECEE674CEF7364B0BC4BEEFB8B66CAD89F98DE2F8C5A5EAD5D1D1E4BD7D04CD"
|
||||
).map(MuSigNoncePriv.fromHex)
|
||||
|
||||
val nonce1 = MuSigNoncePriv.genInternal(rand, sk, aggpk, msg, extraIn)
|
||||
// Vector 1
|
||||
assert(nonce1 == expected(0))
|
||||
|
||||
val nonce2 =
|
||||
MuSigNoncePriv.genInternal(rand, sk, aggpk, msgOpt = None, extraIn)
|
||||
// Vector 2
|
||||
assert(nonce2 == expected(1))
|
||||
|
||||
val nonce3 = MuSigNoncePriv.genInternal(rand)
|
||||
// Vector 3
|
||||
assert(nonce3 == expected(2))
|
||||
}
|
||||
|
||||
// https://github.com/jonasnick/bips/blob/263a765a77e20efe883ed3b28dc155a0d8c7d61a/bip-musig2/reference.py#L461
|
||||
it should "pass nonce aggregation test vectors" in {
|
||||
val pnonce = Vector(
|
||||
"020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E666" ++
|
||||
"03BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641",
|
||||
"03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A6" ++
|
||||
"0248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833"
|
||||
).map(MuSigNoncePub.fromHex)
|
||||
|
||||
val expected = MuSigNoncePub(
|
||||
"035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B" ++
|
||||
"024725377345BDE0E9C33AF3C43C0A29A9249F2F2956FA8CFEB55C8573D0262DC8"
|
||||
)
|
||||
|
||||
// Vector 1
|
||||
assert(MuSigNoncePub.aggregate(pnonce) == expected)
|
||||
|
||||
// The following errors must be handled by the caller as we can't even represent them
|
||||
// Vector 2
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigNoncePub(
|
||||
"04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A6" ++
|
||||
"0248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833"))
|
||||
// Vector 3
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigNoncePub(
|
||||
"03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A6" ++
|
||||
"0248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831"))
|
||||
// Vector 4
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigNoncePub(
|
||||
"03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A6" ++
|
||||
"02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"))
|
||||
|
||||
// Vector 5
|
||||
val g = CryptoParams.getG.toPoint
|
||||
val negG = g.multiply(FieldElement.orderMinusOne)
|
||||
val pnonce1 = MuSigNoncePub(Vector(pnonce.head.pubNonces.head, g))
|
||||
val pnonce2 = MuSigNoncePub(Vector(pnonce.last.pubNonces.head, negG))
|
||||
val expected5 =
|
||||
MuSigNoncePub(expected.bytes.take(33) ++ MuSigNoncePub.infPtBytes)
|
||||
assert(MuSigNoncePub.aggregate(Vector(pnonce1, pnonce2)) == expected5)
|
||||
}
|
||||
|
||||
// https://github.com/jonasnick/bips/blob/263a765a77e20efe883ed3b28dc155a0d8c7d61a/bip-musig2/reference.py#L504
|
||||
it should "pass signing and verification test vectors" in {
|
||||
val remotePubKeys = Vector(
|
||||
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
|
||||
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"
|
||||
).map(SchnorrPublicKey.fromHex)
|
||||
|
||||
val privNonce = MuSigNoncePriv(
|
||||
"508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61" ++
|
||||
"FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")
|
||||
|
||||
val pubNonces = Vector(
|
||||
"0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA" ++
|
||||
"0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",
|
||||
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" ++
|
||||
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
|
||||
"032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE93" ++
|
||||
"03E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046",
|
||||
"0237C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA" ++
|
||||
"0387BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480"
|
||||
).map(MuSigNoncePub.fromHex)
|
||||
|
||||
assert(privNonce.toPublicNonces == pubNonces.head)
|
||||
|
||||
val aggNonce = MuSigNoncePub(
|
||||
"028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61" ++
|
||||
"037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9")
|
||||
|
||||
assert(aggNonce == MuSigNoncePub.aggregate(pubNonces.take(3)))
|
||||
|
||||
val sk = ECPrivateKey(
|
||||
"7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671")
|
||||
val pk = sk.schnorrPublicKey
|
||||
val msg = ByteVector.fromValidHex(
|
||||
"F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF")
|
||||
|
||||
val expected = Vector(
|
||||
"68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B",
|
||||
"2DF67BFFF18E3DE797E13C6475C963048138DAEC5CB20A357CECA7C8424295EA",
|
||||
"0D5B651E6DE34A29A12DE7A8B4183B4AE6A7F7FBE15CDCAFA4A3D1BCAABC7517",
|
||||
"8D5E0407FB4756EEBCD86264C32D792EE36EEB69E952BBB30B8E41BEBC4D22FA"
|
||||
).map(FieldElement.fromHex)
|
||||
|
||||
// Vector 1
|
||||
val keySet1 = UnsortedKeySet(Vector(pk, remotePubKeys(0), remotePubKeys(1)))
|
||||
assert(sign(privNonce, aggNonce, sk, msg, keySet1)._2 == expected(0))
|
||||
|
||||
// Vector 2
|
||||
val keySet2 = UnsortedKeySet(Vector(remotePubKeys(0), pk, remotePubKeys(1)))
|
||||
assert(sign(privNonce, aggNonce, sk, msg, keySet2)._2 == expected(1))
|
||||
|
||||
// Vector 3
|
||||
val keySet3 = UnsortedKeySet(Vector(remotePubKeys(0), remotePubKeys(1), pk))
|
||||
assert(sign(privNonce, aggNonce, sk, msg, keySet3)._2 == expected(2))
|
||||
|
||||
// Vector 4
|
||||
val infAggNonce =
|
||||
MuSigNoncePub.aggregate(Vector(pubNonces(0), pubNonces(3)))
|
||||
assert(infAggNonce.pubNonces.forall(_ == SecpPointInfinity))
|
||||
val keySet4 = UnsortedKeySet(Vector(pk, remotePubKeys(0)))
|
||||
assert(sign(privNonce, infAggNonce, sk, msg, keySet4)._2 == expected(3))
|
||||
|
||||
// The following errors must be handled by the caller as we can't even represent them
|
||||
// Vector 5, 17
|
||||
assertThrows[IllegalArgumentException](
|
||||
SchnorrPublicKey(
|
||||
"0000000000000000000000000000000000000000000000000000000000000007"))
|
||||
// Vector 6
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigNoncePub(
|
||||
"048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61" ++
|
||||
"037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9"))
|
||||
// Vector 7
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigNoncePub(
|
||||
"028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61" ++
|
||||
"020000000000000000000000000000000000000000000000000000000000000009"))
|
||||
// Vector 8
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigNoncePub(
|
||||
"028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61" ++
|
||||
"02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"))
|
||||
|
||||
// Verification test vectors
|
||||
// Vector 9
|
||||
assert(
|
||||
partialSigVerify(expected(0),
|
||||
pubNonces.take(3),
|
||||
keySet1,
|
||||
msg,
|
||||
signerIndex = 0))
|
||||
// Vector 10
|
||||
assert(
|
||||
partialSigVerify(expected(1),
|
||||
Vector(pubNonces(1), pubNonces(0), pubNonces(2)),
|
||||
keySet2,
|
||||
msg,
|
||||
signerIndex = 1))
|
||||
// Vector 11
|
||||
assert(
|
||||
partialSigVerify(expected(2),
|
||||
Vector(pubNonces(1), pubNonces(2), pubNonces(0)),
|
||||
keySet3,
|
||||
msg,
|
||||
signerIndex = 2))
|
||||
|
||||
// Vector 12
|
||||
assert(
|
||||
partialSigVerify(expected(3),
|
||||
Vector(pubNonces(0), pubNonces(3)),
|
||||
keySet4,
|
||||
msg,
|
||||
signerIndex = 0))
|
||||
|
||||
// Vector 13
|
||||
val badSig = FieldElement(
|
||||
"97AC833ADCB1AFA42EBF9E0725616F3C9A0D5B614F6FE283CEAAA37A8FFAF406")
|
||||
assert(!partialSigVerify(badSig, pubNonces, keySet1, msg, signerIndex = 0))
|
||||
// Vector 14
|
||||
assert(
|
||||
!partialSigVerify(expected(0),
|
||||
pubNonces.take(3),
|
||||
keySet1,
|
||||
msg,
|
||||
signerIndex = 1))
|
||||
// The following errors must be handled by the caller as we can't even represent them
|
||||
// Vector 15
|
||||
assertThrows[IllegalArgumentException](
|
||||
FieldElement(
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"))
|
||||
// Vector 16
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigNoncePub(
|
||||
"020000000000000000000000000000000000000000000000000000000000000009" ++
|
||||
pubNonces.head.bytes.drop(33).toHex))
|
||||
}
|
||||
|
||||
// https://github.com/jonasnick/bips/blob/musig2/bip-musig2/reference.py#L629
|
||||
it should "pass tweak test vectors" in {
|
||||
val pubKeys = Vector(
|
||||
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
|
||||
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"
|
||||
).map(SchnorrPublicKey.fromHex)
|
||||
|
||||
val secnonce = MuSigNoncePriv(
|
||||
"508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61" ++
|
||||
"FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")
|
||||
|
||||
val pnonce = Vector(
|
||||
"0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA" ++
|
||||
"0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480",
|
||||
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" ++
|
||||
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
|
||||
"032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE93" ++
|
||||
"03E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046"
|
||||
).map(MuSigNoncePub.fromHex)
|
||||
|
||||
val aggnonce = MuSigNoncePub(
|
||||
"028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61" ++
|
||||
"037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9")
|
||||
|
||||
val sk = ECPrivateKey(
|
||||
"7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671")
|
||||
val msg = ByteVector.fromValidHex(
|
||||
"F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF")
|
||||
|
||||
val tweaks = Vector(
|
||||
"E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB",
|
||||
"AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455",
|
||||
"F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0",
|
||||
"1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D"
|
||||
).map(FieldElement.fromHex)
|
||||
|
||||
val expected = Vector(
|
||||
"5E24C7496B565DEBC3B9639E6F1304A21597F9603D3AB05B4913641775E1375B",
|
||||
"78408DDCAB4813D1394C97D493EF1084195C1D4B52E63ECD7BC5991644E44DDD",
|
||||
"C3A829A81480E36EC3AB052964509A94EBF34210403D16B226A6F16EC85B7357",
|
||||
"8C4473C6A382BD3C4AD7BE59818DA5ED7CF8CEC4BC21996CFDA08BB4316B8BC7"
|
||||
).map(FieldElement.fromHex)
|
||||
|
||||
val pk = sk.schnorrPublicKey
|
||||
|
||||
val keySet = UnsortedKeySet(Vector(pubKeys(0), pubKeys(1), pk))
|
||||
val pnonces = Vector(pnonce(1), pnonce(2), pnonce(0))
|
||||
|
||||
// Vector 1
|
||||
val tweaks1 = Vector(MuSigTweak(tweaks(0), isXOnlyT = true))
|
||||
val keySet1 = keySet.withTweaks(tweaks1)
|
||||
assert(sign(secnonce, aggnonce, sk, msg, keySet1)._2 == expected(0))
|
||||
assert(
|
||||
partialSigVerify(expected(0), pnonces, keySet1, msg, signerIndex = 2))
|
||||
|
||||
// Vector 2
|
||||
val tweaks2 = Vector(MuSigTweak(tweaks(0), isXOnlyT = false))
|
||||
val keySet2 = keySet.withTweaks(tweaks2)
|
||||
assert(sign(secnonce, aggnonce, sk, msg, keySet2)._2 == expected(1))
|
||||
assert(
|
||||
partialSigVerify(expected(1), pnonces, keySet2, msg, signerIndex = 2))
|
||||
|
||||
// Vector 3
|
||||
val tweaks3 = Vector(MuSigTweak(tweaks(0), isXOnlyT = false),
|
||||
MuSigTweak(tweaks(1), isXOnlyT = true))
|
||||
val keySet3 = keySet.withTweaks(tweaks3)
|
||||
assert(sign(secnonce, aggnonce, sk, msg, keySet3)._2 == expected(2))
|
||||
assert(
|
||||
partialSigVerify(expected(2), pnonces, keySet3, msg, signerIndex = 2))
|
||||
|
||||
// Vector 4
|
||||
val tweaks4 = Vector(MuSigTweak(tweaks(0), isXOnlyT = true),
|
||||
MuSigTweak(tweaks(1), isXOnlyT = false),
|
||||
MuSigTweak(tweaks(2), isXOnlyT = true),
|
||||
MuSigTweak(tweaks(3), isXOnlyT = false))
|
||||
val keySet4 = keySet.withTweaks(tweaks4)
|
||||
assert(sign(secnonce, aggnonce, sk, msg, keySet4)._2 == expected(3))
|
||||
assert(
|
||||
partialSigVerify(expected(3), pnonces, keySet4, msg, signerIndex = 2))
|
||||
|
||||
// The following error must be handled by the caller as we can't even represent it
|
||||
// Vector 5
|
||||
assertThrows[IllegalArgumentException](
|
||||
MuSigTweak(
|
||||
FieldElement(
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"),
|
||||
isXOnlyT = false))
|
||||
}
|
||||
|
||||
// https://github.com/jonasnick/bips/blob/263a765a77e20efe883ed3b28dc155a0d8c7d61a/bip-musig2/reference.py#L702
|
||||
it should "pass signature aggregation test vectors" in {
|
||||
val pubKeys = Vector(
|
||||
"487D1B83B41B4CBBD07A111F1BBC7BDC8864CFEF5DBF96E46E51C68399B0BEF6",
|
||||
"4795C22501BF534BC478FF619407A7EC9E8D8883646D69BD43A0728944EA802F",
|
||||
"0F5BE837F3AB7E7FEFF1FAA44D673C2017206AE836D2C7893CDE4ACB7D55EDEB",
|
||||
"0FD453223E444FCA91FB5310990AE8A0C5DAA14D2A4C8944E1C0BC80C30DF682"
|
||||
).map(SchnorrPublicKey.fromHex)
|
||||
|
||||
val aggNonce = Vector(
|
||||
"024FA51009A56F0D6DF737131CE1FBBD833797AF3B4FE6BF0D68F4D49F68B0947E" ++
|
||||
"0248FB3BB9191F0CFF13806A3A2F1429C23012654FCE4E41F7EC9169EAA6056B21",
|
||||
"023B11E63E2460E5E0F1561BB700FEA95B991DD9CA2CBBE92A3960641FA7469F67" ++
|
||||
"02CA4CD38375FE8BEB857C770807225BFC7D712F42BA896B83FC71138E56409B21",
|
||||
"03F98BEAA32B8A38FE3797C4E813DC9CE05ADBE32200035FB37EB0A030B735E9B6" ++
|
||||
"030E6118EC98EA2BA7A358C2E38E7E13E63681EEB683E067061BF7D52DCF08E615",
|
||||
"026491FBCFD47148043A0F7310E62EF898C10F2D0376EE6B232EAAD36F3C2E29E3" ++
|
||||
"03020CB17D168908E2904DE2EB571CD232CA805A6981D0F86CDBBD2F12BD91F6D0",
|
||||
"000000000000000000000000000000000000000000000000000000000000000000" ++
|
||||
"000000000000000000000000000000000000000000000000000000000000000000"
|
||||
).map(MuSigNoncePub.fromHex)
|
||||
|
||||
val msg = ByteVector.fromValidHex(
|
||||
"599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869")
|
||||
|
||||
val tweaks = Vector(
|
||||
"B511DA492182A91B0FFB9A98020D55F260AE86D7ECBD0399C7383D59A5F2AF7C",
|
||||
"A815FE049EE3C5AAB66310477FBC8BCCCAC2F3395F59F921C364ACD78A2F48DC",
|
||||
"75448A87274B056468B977BE06EB1E9F657577B7320B0A3376EA51FD420D18A8"
|
||||
).map(FieldElement.fromHex)
|
||||
|
||||
val psig = Vector(
|
||||
"E5C1CBD6E7E89FE9EE30D5F3B6D06B9C218846E4A1DEF4EE851410D51ABBD850",
|
||||
"9BC470F7F1C9BC848BDF179B0023282FFEF40908E0EF88459784A4355FC86D0C",
|
||||
"D5D8A09929BA264B2F5DF15ACA1CF2DEFA47C048DF0C3232E965FFE2F2831B1D",
|
||||
"A915197503C1051EA77DC91F01C3A0E60BFD64473BD536CB613F9645BD61C843",
|
||||
"99A144D7076A128022134E036B8BDF33811F7EAED9A1E48549B46D8A63D64DC9",
|
||||
"716A72A0C1E531EBB4555C8E29FD35C796F4F231C3B039193D7E8D7AEFBDF5F7",
|
||||
"06B6DD04BC0F1EF740916730AD7DAC794255B161221719765BDE9686A26633DC",
|
||||
"BF6D85D4930062726EBC6EBB184AFD68DBB3FED159C501989690A62600D6FBAB"
|
||||
).map(FieldElement.fromHex)
|
||||
|
||||
val expected = Vector(
|
||||
"4006D4D069F3B51E968762FF8074153E278E5BCD221AABE0743CA001B77E79F5" ++
|
||||
"81863CCED9B25C6E7A0FED8EB6F393CD65CD7306D385DCF85CC6567DAA4E041B",
|
||||
"98BCD40DFD94B47A3DA37D7B78EB6CCE8ABEACA23C3ADE6F4678902410EB35C6" ++
|
||||
"7EEDBA0E2D7B2B69D6DBBA79CBE093C64B9647A96B98C8C28AD3379BDFAEA21F",
|
||||
"3741FEDCCDD7508B58DCB9A780FF5D97452EC8C0448D8C97004EA7175C14F200" ++
|
||||
"7A54D1DE356EBA6719278436EF111DFA8F1B832368371B9B7A25001709039679",
|
||||
"F4B3DA3CF0D0F7CF5C1840593BF1A1A415DA341619AE848F2210696DC8C75125" ++
|
||||
"40962C84EF7F0CEC491065F2D577213CF10E8A63D153297361B3B172BE27B61F"
|
||||
).map(SchnorrDigitalSignature.fromHex)
|
||||
|
||||
// Vector 1
|
||||
val keySet1 = UnsortedKeySet(pubKeys.take(2))
|
||||
val sig1 = signAgg(psig.take(2), aggNonce(0), keySet1, msg)
|
||||
assert(sig1 == expected(0))
|
||||
assert(keySet1.aggPubKey.schnorrPublicKey.verify(msg, sig1))
|
||||
|
||||
// Vector 2
|
||||
val keySet2 = UnsortedKeySet(Vector(pubKeys(0), pubKeys(2)))
|
||||
val sig2 = signAgg(psig.slice(2, 4), aggNonce(1), keySet2, msg)
|
||||
assert(sig2 == expected(1))
|
||||
assert(keySet2.aggPubKey.schnorrPublicKey.verify(msg, sig2))
|
||||
|
||||
// Vector 3
|
||||
val tweaks3 = Vector(MuSigTweak(tweaks(0), isXOnlyT = false))
|
||||
val keySet3 = UnsortedKeySet(Vector(pubKeys(0), pubKeys(2)), tweaks3)
|
||||
val sig3 = signAgg(psig.slice(4, 6), aggNonce(2), keySet3, msg)
|
||||
assert(sig3 == expected(2))
|
||||
assert(keySet3.aggPubKey.schnorrPublicKey.verify(msg, sig3))
|
||||
|
||||
// Vector 4
|
||||
val tweaks4 = Vector(MuSigTweak(tweaks(0), isXOnlyT = true),
|
||||
MuSigTweak(tweaks(1), isXOnlyT = false),
|
||||
MuSigTweak(tweaks(2), isXOnlyT = true))
|
||||
val keySet4 = UnsortedKeySet(Vector(pubKeys(0), pubKeys(3)), tweaks4)
|
||||
val sig4 = signAgg(psig.slice(6, 8), aggNonce(3), keySet4, msg)
|
||||
assert(sig4 == expected(3))
|
||||
assert(keySet4.aggPubKey.schnorrPublicKey.verify(msg, sig4))
|
||||
|
||||
// The following error must be handled by the caller as we can't even represent it
|
||||
// Vector 5
|
||||
assertThrows[IllegalArgumentException](
|
||||
FieldElement(
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"))
|
||||
}
|
||||
}
|
@ -211,6 +211,26 @@ trait CryptoRuntime {
|
||||
|
||||
def tweakMultiply(publicKey: ECPublicKey, tweak: FieldElement): ECPublicKey
|
||||
|
||||
def tweakMultiply(point: SecpPoint, tweak: FieldElement): SecpPoint = {
|
||||
point match {
|
||||
case SecpPointInfinity => SecpPointInfinity
|
||||
case pt: SecpPointFinite =>
|
||||
val pub = pt.toPublicKey
|
||||
Try(tweakMultiply(pub, tweak)) match {
|
||||
case Failure(err) =>
|
||||
/* If tweak*pub = 0, then (tweak - 1)*pub + pub = 0
|
||||
* so (tweak - 1)*pub = -pub. Otherwise err is unrelated.
|
||||
* (Noting that it is impossible to have
|
||||
* tweak*pub = 0 = (tweak - 1)*pub for pub != 0)
|
||||
*/
|
||||
val pubNeg = tweakMultiply(pub, tweak.subtract(FieldElement.one))
|
||||
if (pubNeg == pub.negate) SecpPointInfinity
|
||||
else throw err
|
||||
case Success(multKey) => multKey.toPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def add(pk1: ECPrivateKey, pk2: ECPrivateKey): ECPrivateKey =
|
||||
pk1.fieldElement.add(pk2.fieldElement).toPrivateKey
|
||||
|
||||
|
@ -398,6 +398,10 @@ case class ECPublicKey(private val _bytes: ByteVector)
|
||||
def toXOnly: XOnlyPubKey = XOnlyPubKey(bytes.drop(1))
|
||||
|
||||
def parity: KeyParity = KeyParity.fromByte(bytes.head)
|
||||
|
||||
def negate: ECPublicKey = {
|
||||
ECPublicKey.fromBytes(parity.negate.bytes ++ bytes.drop(1))
|
||||
}
|
||||
}
|
||||
|
||||
object ECPublicKey extends Factory[ECPublicKey] {
|
||||
|
@ -5,6 +5,13 @@ import scodec.bits.ByteVector
|
||||
sealed trait KeyParity extends NetworkElement {
|
||||
def isOdd: Boolean = this == OddParity
|
||||
def isEven: Boolean = this == EvenParity
|
||||
|
||||
def negate: KeyParity = {
|
||||
this match {
|
||||
case EvenParity => OddParity
|
||||
case OddParity => EvenParity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object KeyParity extends Factory[KeyParity] {
|
||||
|
@ -23,3 +23,10 @@ trait NetworkElement extends Any {
|
||||
/** The byte representation of the NetworkElement in little endian */
|
||||
def bytesLE: ByteVector = bytes.reverse
|
||||
}
|
||||
|
||||
object NetworkElement {
|
||||
|
||||
val lexicographicalOrdering: Ordering[NetworkElement] = {
|
||||
Ordering.by[NetworkElement, String](_.hex)
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,17 @@ sealed trait SecpPoint extends NetworkElement {
|
||||
def add(point: SecpPoint): SecpPoint = {
|
||||
CryptoUtil.add(this, point)
|
||||
}
|
||||
|
||||
def multiply(fieldElement: FieldElement): SecpPoint = {
|
||||
CryptoUtil.tweakMultiply(this, fieldElement)
|
||||
}
|
||||
|
||||
def negate: SecpPoint = {
|
||||
this match {
|
||||
case SecpPointInfinity => SecpPointInfinity
|
||||
case p: SecpPointFinite => p.toPublicKey.negate.toPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The point at infinity, this is the secp256k1 group identity element meaning
|
||||
|
116
crypto/src/main/scala/org/bitcoins/crypto/musig/KeySet.scala
Normal file
116
crypto/src/main/scala/org/bitcoins/crypto/musig/KeySet.scala
Normal file
@ -0,0 +1,116 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto.{
|
||||
ECPublicKey,
|
||||
FieldElement,
|
||||
NetworkElement,
|
||||
SchnorrPublicKey
|
||||
}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/** Represents an ordered set of MuSig signers and their tweaks.
|
||||
* This is the data required to (non-interactively) compute the aggPubKey.
|
||||
*/
|
||||
trait KeySet {
|
||||
def keys: Vector[SchnorrPublicKey]
|
||||
|
||||
def tweaks: Vector[MuSigTweak]
|
||||
|
||||
def withTweaks(newTweaks: Vector[MuSigTweak]): KeySet = {
|
||||
require(tweaks.isEmpty, "withTweaks is not meant for replacing tweaks")
|
||||
|
||||
this match {
|
||||
case ks: LexicographicKeySet => ks.copy(tweaks = newTweaks)
|
||||
case ks: UnsortedKeySet => ks.copy(tweaks = newTweaks)
|
||||
}
|
||||
}
|
||||
|
||||
def length: Int = keys.length
|
||||
|
||||
def apply(i: Int): SchnorrPublicKey = {
|
||||
keys(i)
|
||||
}
|
||||
|
||||
lazy val serialize: ByteVector = {
|
||||
keys.map(_.bytes).reduce(_ ++ _)
|
||||
}
|
||||
|
||||
/** Returns the coefficient of the given key in the aggPubKey sum */
|
||||
def keyAggCoef(key: SchnorrPublicKey): FieldElement = {
|
||||
if (secondKeyOpt.contains(key)) FieldElement.one
|
||||
else {
|
||||
val listHashBytes = MuSigUtil.aggListHash(serialize)
|
||||
val bytes = MuSigUtil.aggCoefHash(listHashBytes ++ key.bytes)
|
||||
|
||||
FieldElement(new java.math.BigInteger(1, bytes.toArray))
|
||||
}
|
||||
}
|
||||
|
||||
private lazy val computeAggPubKeyAndTweakContext: (
|
||||
ECPublicKey,
|
||||
MuSigTweakContext) = {
|
||||
val untweakedAggPubKey = keys
|
||||
.map { key =>
|
||||
val coef = keyAggCoef(key)
|
||||
key.publicKey.multiply(coef)
|
||||
}
|
||||
.reduce(_.add(_))
|
||||
|
||||
tweaks.foldLeft((untweakedAggPubKey, MuSigTweakContext.empty)) {
|
||||
case ((pubKeySoFar, context), tweak) =>
|
||||
context.applyTweak(tweak, pubKeySoFar)
|
||||
}
|
||||
}
|
||||
|
||||
/** The aggregate public key that represents the n-of-n signers */
|
||||
lazy val aggPubKey: ECPublicKey = computeAggPubKeyAndTweakContext._1
|
||||
|
||||
/** Accumulated tweak information */
|
||||
lazy val tweakContext: MuSigTweakContext =
|
||||
computeAggPubKeyAndTweakContext._2
|
||||
|
||||
/** The first key different from the keys.head,
|
||||
* optimized MuSig2 allows this key to have coefficient 1
|
||||
*/
|
||||
lazy val secondKeyOpt: Option[SchnorrPublicKey] = {
|
||||
keys.find(_ != keys.head)
|
||||
}
|
||||
}
|
||||
|
||||
object KeySet {
|
||||
|
||||
def apply(keys: Vector[SchnorrPublicKey]): LexicographicKeySet = {
|
||||
val sortedKeys = keys.sorted(NetworkElement.lexicographicalOrdering)
|
||||
LexicographicKeySet(sortedKeys)
|
||||
}
|
||||
|
||||
def apply(keys: SchnorrPublicKey*): LexicographicKeySet = {
|
||||
KeySet(keys.toVector)
|
||||
}
|
||||
|
||||
def apply(
|
||||
keys: Vector[SchnorrPublicKey],
|
||||
tweaks: Vector[MuSigTweak]): LexicographicKeySet = {
|
||||
val sortedKeys = keys.sorted(NetworkElement.lexicographicalOrdering)
|
||||
LexicographicKeySet(sortedKeys, tweaks)
|
||||
}
|
||||
}
|
||||
|
||||
/** The default way of ordering a KeySet is lexicographically */
|
||||
case class LexicographicKeySet(
|
||||
override val keys: Vector[SchnorrPublicKey],
|
||||
override val tweaks: Vector[MuSigTweak] = Vector.empty)
|
||||
extends KeySet {
|
||||
keys.init.zip(keys.tail).foreach { case (key1, key2) =>
|
||||
require(key1.hex.compareTo(key2.hex) <= 0,
|
||||
"Keys must be sorted lexicographically")
|
||||
}
|
||||
}
|
||||
|
||||
/** This represents an arbitrary KeySet, for use in tests.
|
||||
* If you have a non-lexicographical order, extend KeySet.
|
||||
*/
|
||||
case class UnsortedKeySet(
|
||||
override val keys: Vector[SchnorrPublicKey],
|
||||
override val tweaks: Vector[MuSigTweak] = Vector.empty)
|
||||
extends KeySet
|
@ -0,0 +1,112 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/** Wraps the ephemeral private keys making up a MuSig2 nonce */
|
||||
case class MuSigNoncePriv(privNonces: Vector[ECPrivateKey])
|
||||
extends NetworkElement {
|
||||
require(privNonces.length == MuSigUtil.nonceNum,
|
||||
s"Exactly ${MuSigUtil.nonceNum} keys are expected, found $privNonces")
|
||||
|
||||
def toPublicNonces: MuSigNoncePub = {
|
||||
MuSigNoncePub(privNonces.map(_.publicKey.toPoint))
|
||||
}
|
||||
|
||||
def toFieldElements: Vector[FieldElement] = {
|
||||
privNonces.map(_.fieldElement)
|
||||
}
|
||||
|
||||
def length: Int = privNonces.length
|
||||
|
||||
override def bytes: ByteVector = {
|
||||
privNonces.map(_.bytes).reduce(_ ++ _)
|
||||
}
|
||||
|
||||
def negate: MuSigNoncePriv = {
|
||||
MuSigNoncePriv(privNonces.map(_.negate))
|
||||
}
|
||||
|
||||
/** Collapses this into a single ephemeral private key */
|
||||
def sumToKey(b: FieldElement): FieldElement = {
|
||||
MuSigUtil.nonceSum[FieldElement](toFieldElements,
|
||||
b,
|
||||
_.add(_),
|
||||
_.multiply(_),
|
||||
FieldElement.zero)
|
||||
}
|
||||
}
|
||||
|
||||
object MuSigNoncePriv extends Factory[MuSigNoncePriv] {
|
||||
|
||||
override def fromBytes(bytes: ByteVector): MuSigNoncePriv = {
|
||||
val privs = bytes.toArray
|
||||
.grouped(32)
|
||||
.toVector
|
||||
.map(ByteVector(_))
|
||||
.map(ECPrivateKey.fromBytes)
|
||||
|
||||
MuSigNoncePriv(privs)
|
||||
}
|
||||
|
||||
/** Generates a MuSigNoncePriv given 32 bytes of entropy from preRand,
|
||||
* and possibly some other sources, as specified in the BIP.
|
||||
*/
|
||||
def genInternal(
|
||||
preRand: ByteVector,
|
||||
privKeyOpt: Option[ECPrivateKey] = None,
|
||||
aggPubKeyOpt: Option[SchnorrPublicKey] = None,
|
||||
msgOpt: Option[ByteVector] = None,
|
||||
extraInOpt: Option[ByteVector] = None): MuSigNoncePriv = {
|
||||
require(preRand.length == 32,
|
||||
s"32 bytes of entropy must be provided, found $preRand")
|
||||
require(msgOpt.forall(msg => msg.length == 32),
|
||||
s"The message to be signed must be 32 bytes, found $msgOpt")
|
||||
require(
|
||||
extraInOpt.forall(_.length <= 4294967295L),
|
||||
"extraIn too long, its length must be represented by at most four bytes")
|
||||
|
||||
def serializeWithLen(
|
||||
bytesOpt: Option[ByteVector],
|
||||
lengthSize: Int = 1): ByteVector = {
|
||||
bytesOpt match {
|
||||
case Some(bytes) =>
|
||||
ByteVector.fromLong(bytes.length, lengthSize) ++ bytes
|
||||
case None => ByteVector.fromLong(0, lengthSize)
|
||||
}
|
||||
}
|
||||
|
||||
val rand = privKeyOpt match {
|
||||
case Some(privKey) => MuSigUtil.auxHash(preRand).xor(privKey.bytes)
|
||||
case None => preRand
|
||||
}
|
||||
|
||||
val aggPubKeyBytes = serializeWithLen(aggPubKeyOpt.map(_.bytes))
|
||||
val msgBytes = serializeWithLen(msgOpt)
|
||||
val extraInBytes = serializeWithLen(extraInOpt, lengthSize = 4)
|
||||
val dataBytes = rand ++ aggPubKeyBytes ++ msgBytes ++ extraInBytes
|
||||
|
||||
val privNonceKeys = 0.until(MuSigUtil.nonceNum).toVector.map { index =>
|
||||
val indexByte = ByteVector.fromByte(index.toByte)
|
||||
val noncePreBytes = MuSigUtil.nonHash(dataBytes ++ indexByte)
|
||||
val noncePreNum = new java.math.BigInteger(1, noncePreBytes.toArray)
|
||||
|
||||
FieldElement(noncePreNum).toPrivateKey
|
||||
}
|
||||
|
||||
MuSigNoncePriv(privNonceKeys)
|
||||
}
|
||||
|
||||
/** Generates 32 bytes of entropy and contructs a MuSigNoncePriv from this,
|
||||
* and possibly some other sources, as specified in the BIP.
|
||||
*/
|
||||
def gen(
|
||||
privKeyOpt: Option[ECPrivateKey] = None,
|
||||
aggPubKeyOpt: Option[SchnorrPublicKey] = None,
|
||||
msgOpt: Option[ByteVector] = None,
|
||||
extraInOpt: Option[ByteVector] = None): MuSigNoncePriv = {
|
||||
val preRand = CryptoUtil.randomBytes(32)
|
||||
|
||||
genInternal(preRand, privKeyOpt, aggPubKeyOpt, msgOpt, extraInOpt)
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/** Wraps the ephemeral points making up a MuSig2 nonce */
|
||||
case class MuSigNoncePub(pubNonces: Vector[SecpPoint]) extends NetworkElement {
|
||||
require(pubNonces.length == MuSigUtil.nonceNum,
|
||||
s"Exactly ${MuSigUtil.nonceNum} keys are expected, found $pubNonces")
|
||||
|
||||
def apply(i: Int): SecpPoint = {
|
||||
pubNonces(i)
|
||||
}
|
||||
|
||||
def length: Int = pubNonces.length
|
||||
|
||||
override def bytes: ByteVector = {
|
||||
pubNonces
|
||||
.map {
|
||||
case SecpPointInfinity => MuSigNoncePub.infPtBytes
|
||||
case p: SecpPointFinite => p.toPublicKey.bytes
|
||||
}
|
||||
.reduce(_ ++ _)
|
||||
}
|
||||
|
||||
/** Collapses this into a single ephemeral public key */
|
||||
def sumToKey(b: FieldElement): ECPublicKey = {
|
||||
MuSigUtil.nonceSum[SecpPoint](pubNonces,
|
||||
b,
|
||||
_.add(_),
|
||||
_.multiply(_),
|
||||
SecpPointInfinity) match {
|
||||
case SecpPointInfinity => CryptoParams.getG
|
||||
case p: SecpPointFinite => p.toPublicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MuSigNoncePub extends Factory[MuSigNoncePub] {
|
||||
|
||||
/** In the BIP, the point at infinity is serialized as 33 0x00 bytes */
|
||||
val infPtBytes: ByteVector = ByteVector.low(33)
|
||||
|
||||
override def fromBytes(bytes: ByteVector): MuSigNoncePub = {
|
||||
val pubs =
|
||||
bytes.toArray
|
||||
.grouped(33)
|
||||
.toVector
|
||||
.map(ByteVector(_))
|
||||
.map { b =>
|
||||
if (b == infPtBytes) SecpPointInfinity
|
||||
else ECPublicKey.fromBytes(b).toPoint
|
||||
}
|
||||
|
||||
MuSigNoncePub(pubs)
|
||||
}
|
||||
|
||||
/** Sums the given nonces and returns the aggregate MuSigNoncePub */
|
||||
def aggregate(nonces: Vector[MuSigNoncePub]): MuSigNoncePub = {
|
||||
val aggNonceKeys = 0.until(MuSigUtil.nonceNum).toVector.map { i =>
|
||||
nonces.map(multiNonce => multiNonce(i)).reduce(_.add(_))
|
||||
}
|
||||
|
||||
MuSigNoncePub(aggNonceKeys)
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto.{ECPublicKey, FieldElement}
|
||||
|
||||
/** Used to tweak a MuSig aggregate public key, as defined here
|
||||
* https://github.com/jonasnick/bips/blob/musig2/bip-musig2.mediawiki#tweaking-definition
|
||||
*/
|
||||
case class MuSigTweak(tweak: FieldElement, isXOnlyT: Boolean) {
|
||||
def point: ECPublicKey = tweak.getPublicKey
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto.{ECPublicKey, FieldElement, OddParity}
|
||||
|
||||
/** Represents the total tweak sum and net parity multiplier
|
||||
* after applying all tweaks
|
||||
*/
|
||||
case class MuSigTweakContext(
|
||||
parityAcc: ParityMultiplier,
|
||||
tweakAcc: FieldElement) {
|
||||
|
||||
/** Adds tweak to tweakAcc and aggPubKey changing parityAcc if necessary */
|
||||
def applyTweak(
|
||||
tweak: MuSigTweak,
|
||||
aggPubKey: ECPublicKey): (ECPublicKey, MuSigTweakContext) = {
|
||||
val parityMult =
|
||||
if (tweak.isXOnlyT && aggPubKey.parity == OddParity) Neg
|
||||
else Pos
|
||||
|
||||
val newAggPubKey = parityMult.modify(aggPubKey).add(tweak.point)
|
||||
val newParityAcc = parityAcc.multiply(parityMult)
|
||||
val newTweakAcc = parityMult.modify(tweakAcc).add(tweak.tweak)
|
||||
(newAggPubKey, MuSigTweakContext(newParityAcc, newTweakAcc))
|
||||
}
|
||||
}
|
||||
|
||||
object MuSigTweakContext {
|
||||
|
||||
/** The MuSigTweakContext for when there are no tweaks */
|
||||
val empty: MuSigTweakContext =
|
||||
MuSigTweakContext(Pos, FieldElement.zero)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto.{EvenParity, FieldElement, KeyParity, OddParity}
|
||||
|
||||
/** The data required to apply the net tweak during
|
||||
* MuSig signature aggregation
|
||||
*/
|
||||
case class MuSigTweakData(
|
||||
context: MuSigTweakContext,
|
||||
aggPubKeyParity: KeyParity,
|
||||
e: FieldElement) {
|
||||
|
||||
def additiveTweak: FieldElement = {
|
||||
val g = aggPubKeyParity match {
|
||||
case EvenParity => FieldElement.one
|
||||
case OddParity => FieldElement.orderMinusOne
|
||||
}
|
||||
|
||||
e.multiply(g).multiply(context.tweakAcc)
|
||||
}
|
||||
}
|
170
crypto/src/main/scala/org/bitcoins/crypto/musig/MuSigUtil.scala
Normal file
170
crypto/src/main/scala/org/bitcoins/crypto/musig/MuSigUtil.scala
Normal file
@ -0,0 +1,170 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
// TODO test against secp256k1-zkp someday
|
||||
/** Contains constants, hash functions, and signing/verification functionality for MuSig */
|
||||
object MuSigUtil {
|
||||
|
||||
val nonceNum: Int = 2
|
||||
|
||||
def aggListHash(bytes: ByteVector): ByteVector = {
|
||||
CryptoUtil.taggedSha256(bytes, "KeyAgg list").bytes
|
||||
}
|
||||
|
||||
def aggCoefHash(bytes: ByteVector): ByteVector = {
|
||||
CryptoUtil.taggedSha256(bytes, "KeyAgg coefficient").bytes
|
||||
}
|
||||
|
||||
def nonHash(bytes: ByteVector): ByteVector = {
|
||||
CryptoUtil.taggedSha256(bytes, "MuSig/nonce").bytes
|
||||
}
|
||||
|
||||
def nonCoefHash(bytes: ByteVector): ByteVector = {
|
||||
CryptoUtil.taggedSha256(bytes, "MuSig/noncecoef").bytes
|
||||
}
|
||||
|
||||
def auxHash(bytes: ByteVector): ByteVector = {
|
||||
CryptoUtil.taggedSha256(bytes, "MuSig/aux").bytes
|
||||
}
|
||||
|
||||
/** nonces(0) + nonces(1)*b + nonces(2)*b*b + ... */
|
||||
private[musig] def nonceSum[T](
|
||||
nonces: Vector[T],
|
||||
b: FieldElement,
|
||||
add: (T, T) => T,
|
||||
multiply: (T, FieldElement) => T,
|
||||
identity: T): T = {
|
||||
nonces
|
||||
.foldLeft((FieldElement.one, identity)) { case ((pow, sumSoFar), nonce) =>
|
||||
val prod = multiply(nonce, pow)
|
||||
|
||||
(pow.multiply(b), add(sumSoFar, prod))
|
||||
}
|
||||
._2
|
||||
}
|
||||
|
||||
/** Generates a MuSig partial signature, accompanied by the aggregate R value */
|
||||
def sign(
|
||||
noncePriv: MuSigNoncePriv,
|
||||
aggNoncePub: MuSigNoncePub,
|
||||
privKey: ECPrivateKey,
|
||||
message: ByteVector,
|
||||
keySet: KeySet): (ECPublicKey, FieldElement) = {
|
||||
val pubKey = privKey.publicKey
|
||||
val coef = keySet.keyAggCoef(pubKey.schnorrPublicKey)
|
||||
val SigningSession(b, aggNonce, e) =
|
||||
SigningSession(aggNoncePub, keySet, message)
|
||||
|
||||
val adjustedNoncePriv = aggNonce.parity match {
|
||||
case EvenParity => noncePriv
|
||||
case OddParity => noncePriv.negate
|
||||
}
|
||||
|
||||
val gp = ParityMultiplier.fromParity(pubKey.parity)
|
||||
|
||||
val g = ParityMultiplier.fromParity(keySet.aggPubKey.parity)
|
||||
|
||||
val adjustedPrivKey = gp
|
||||
.multiply(g)
|
||||
.multiply(keySet.tweakContext.parityAcc)
|
||||
.modify(privKey.fieldElement)
|
||||
|
||||
val privNonceSum = adjustedNoncePriv.sumToKey(b)
|
||||
|
||||
val s = adjustedPrivKey.multiply(e).multiply(coef).add(privNonceSum)
|
||||
|
||||
require(partialSigVerify(s,
|
||||
noncePriv.toPublicNonces,
|
||||
pubKey.schnorrPublicKey,
|
||||
keySet,
|
||||
b,
|
||||
aggNonce,
|
||||
e),
|
||||
"Failed verification when generating signature.")
|
||||
|
||||
(aggNonce, s)
|
||||
}
|
||||
|
||||
def partialSigVerify(
|
||||
partialSig: FieldElement,
|
||||
pubNonces: Vector[MuSigNoncePub],
|
||||
keySet: KeySet,
|
||||
message: ByteVector,
|
||||
signerIndex: Int): Boolean = {
|
||||
require(signerIndex >= 0 && signerIndex < keySet.length,
|
||||
s"Invalid signer index $signerIndex for ${keySet.length} signers")
|
||||
|
||||
partialSigVerify(partialSig,
|
||||
pubNonces(signerIndex),
|
||||
MuSigNoncePub.aggregate(pubNonces),
|
||||
keySet(signerIndex),
|
||||
keySet,
|
||||
message)
|
||||
}
|
||||
|
||||
def partialSigVerify(
|
||||
partialSig: FieldElement,
|
||||
noncePub: MuSigNoncePub,
|
||||
aggNoncePub: MuSigNoncePub,
|
||||
pubKey: SchnorrPublicKey,
|
||||
keySet: KeySet,
|
||||
message: ByteVector): Boolean = {
|
||||
val SigningSession(b, aggNonce, e) =
|
||||
SigningSession(aggNoncePub, keySet, message)
|
||||
|
||||
partialSigVerify(partialSig, noncePub, pubKey, keySet, b, aggNonce, e)
|
||||
}
|
||||
|
||||
def partialSigVerify(
|
||||
partialSig: FieldElement,
|
||||
noncePub: MuSigNoncePub,
|
||||
pubKey: SchnorrPublicKey,
|
||||
keySet: KeySet,
|
||||
b: FieldElement,
|
||||
aggNonce: ECPublicKey,
|
||||
e: FieldElement): Boolean = {
|
||||
val nonceSum = noncePub.sumToKey(b)
|
||||
val nonceSumAdjusted = aggNonce.parity match {
|
||||
case EvenParity => nonceSum
|
||||
case OddParity => nonceSum.negate
|
||||
}
|
||||
|
||||
val g = ParityMultiplier.fromParity(keySet.aggPubKey.parity)
|
||||
val aggKeyParity = g.multiply(keySet.tweakContext.parityAcc).toParity
|
||||
|
||||
val aggKey = pubKey.toXOnly.publicKey(aggKeyParity)
|
||||
val a = keySet.keyAggCoef(pubKey)
|
||||
partialSig.getPublicKey == nonceSumAdjusted.add(
|
||||
aggKey.multiply(e.multiply(a)))
|
||||
}
|
||||
|
||||
/** Aggregates MuSig partial signatures into a BIP340 SchnorrDigitalSignature */
|
||||
def signAgg(
|
||||
sVals: Vector[FieldElement],
|
||||
aggNoncePub: MuSigNoncePub,
|
||||
keySet: KeySet,
|
||||
message: ByteVector): SchnorrDigitalSignature = {
|
||||
val SigningSession(_, aggNonce, e) =
|
||||
SigningSession(aggNoncePub, keySet, message)
|
||||
val tweakData =
|
||||
MuSigTweakData(keySet.tweakContext, keySet.aggPubKey.parity, e)
|
||||
|
||||
signAgg(sVals, aggNonce, Some(tweakData))
|
||||
}
|
||||
|
||||
/** Aggregates MuSig partial signatures into a BIP340 SchnorrDigitalSignature */
|
||||
def signAgg(
|
||||
sVals: Vector[FieldElement],
|
||||
aggPubNonce: ECPublicKey,
|
||||
tweakDataOpt: Option[MuSigTweakData] = None): SchnorrDigitalSignature = {
|
||||
val sSum = sVals.reduce(_.add(_))
|
||||
val s = tweakDataOpt match {
|
||||
case Some(tweakData) => sSum.add(tweakData.additiveTweak)
|
||||
case None => sSum
|
||||
}
|
||||
|
||||
SchnorrDigitalSignature(aggPubNonce.schnorrNonce, s)
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto.{
|
||||
ECPublicKey,
|
||||
EvenParity,
|
||||
FieldElement,
|
||||
KeyParity,
|
||||
OddParity
|
||||
}
|
||||
|
||||
/** Represents either FieldElement.one or FieldElement.orderMinusOne.
|
||||
* Using this ADT rather than those actual FieldElements saves computation
|
||||
* including some unnecessary point multiplications.
|
||||
*
|
||||
* In general there is a correspondence between Pos <-> EvenParity and Neg <-> OddParity,
|
||||
* this is because in general x-only keys are assumed to be even and need to be negated
|
||||
* if they are meant to be used as odd keys.
|
||||
*/
|
||||
sealed trait ParityMultiplier {
|
||||
|
||||
def modify(fieldElement: FieldElement): FieldElement = {
|
||||
this match {
|
||||
case Pos => fieldElement
|
||||
case Neg => fieldElement.negate
|
||||
}
|
||||
}
|
||||
|
||||
def modify(pubKey: ECPublicKey): ECPublicKey = {
|
||||
this match {
|
||||
case Pos => pubKey
|
||||
case Neg => pubKey.negate
|
||||
}
|
||||
}
|
||||
|
||||
/** Combines two ParityMultiplier into a single one representing their net modification */
|
||||
def multiply(other: ParityMultiplier): ParityMultiplier = {
|
||||
(this, other) match {
|
||||
case (Pos, Pos) | (Neg, Neg) => Pos
|
||||
case (Pos, Neg) | (Neg, Pos) => Neg
|
||||
}
|
||||
}
|
||||
|
||||
def toParity: KeyParity = {
|
||||
this match {
|
||||
case Pos => EvenParity
|
||||
case Neg => OddParity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ParityMultiplier {
|
||||
|
||||
def fromParity(keyParity: KeyParity): ParityMultiplier = {
|
||||
keyParity match {
|
||||
case EvenParity => Pos
|
||||
case OddParity => Neg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents FieldElement.one */
|
||||
case object Pos extends ParityMultiplier
|
||||
|
||||
/** Represents FieldElement.orderMinusOne */
|
||||
case object Neg extends ParityMultiplier
|
@ -0,0 +1,61 @@
|
||||
package org.bitcoins.crypto.musig
|
||||
|
||||
import org.bitcoins.crypto.{
|
||||
CryptoUtil,
|
||||
ECPublicKey,
|
||||
FieldElement,
|
||||
SchnorrPublicKey
|
||||
}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/** The data relevant to computing and verifying MuSig partial signatures */
|
||||
case class SigningSession(
|
||||
b: FieldElement,
|
||||
aggNonce: ECPublicKey,
|
||||
e: FieldElement)
|
||||
|
||||
object SigningSession {
|
||||
|
||||
def computeB(
|
||||
aggNoncePub: MuSigNoncePub,
|
||||
keySet: KeySet,
|
||||
message: ByteVector): FieldElement = {
|
||||
val aggPubKey = keySet.aggPubKey.schnorrPublicKey
|
||||
|
||||
val bHash =
|
||||
MuSigUtil.nonCoefHash(aggNoncePub.bytes ++ aggPubKey.bytes ++ message)
|
||||
|
||||
FieldElement(new java.math.BigInteger(1, bHash.toArray))
|
||||
}
|
||||
|
||||
def computeE(
|
||||
aggPubKey: SchnorrPublicKey,
|
||||
aggNonce: ECPublicKey,
|
||||
message: ByteVector): FieldElement = {
|
||||
val eBytes = CryptoUtil
|
||||
.sha256SchnorrChallenge(
|
||||
aggNonce.schnorrNonce.bytes ++ aggPubKey.bytes ++ message)
|
||||
.bytes
|
||||
|
||||
FieldElement(new java.math.BigInteger(1, eBytes.toArray))
|
||||
}
|
||||
|
||||
def getSessionValues(
|
||||
aggNoncePub: MuSigNoncePub,
|
||||
keySet: KeySet,
|
||||
message: ByteVector): SigningSession = {
|
||||
val aggPubKey = keySet.aggPubKey.schnorrPublicKey
|
||||
val b = computeB(aggNoncePub, keySet, message)
|
||||
val aggNonce = aggNoncePub.sumToKey(b)
|
||||
val e = computeE(aggPubKey, aggNonce, message)
|
||||
|
||||
SigningSession(b, aggNonce, e)
|
||||
}
|
||||
|
||||
def apply(
|
||||
aggNoncePub: MuSigNoncePub,
|
||||
keySet: KeySet,
|
||||
message: ByteVector): SigningSession = {
|
||||
getSessionValues(aggNoncePub, keySet, message)
|
||||
}
|
||||
}
|
94
docs/crypto/musig.md
Normal file
94
docs/crypto/musig.md
Normal file
@ -0,0 +1,94 @@
|
||||
---
|
||||
id: musig
|
||||
title: MuSig
|
||||
---
|
||||
|
||||
Bitcoin-S now has support for [MuSig](https://github.com/jonasnick/bips/blob/musig2/bip-musig2.mediawiki).
|
||||
|
||||
This module contains classes representing public `KeySet`s, MuSig nonces, and MuSig aggregate key tweaks, as well as utility functions for all MuSig computations.
|
||||
|
||||
The functions for aggregating key data are:
|
||||
|
||||
* `aggPubKey`
|
||||
* This is a member of `KeySet` and returns the aggregate public key for this set of signers, including the tweaks provided. In most uses, a subsequent call to `schnorrPublicKey` is required for Bitcoin applications.
|
||||
* `MuSigNoncePub.aggregate`
|
||||
* Given a `Vector[MuSigNoncePub]` of the signer's nonces, returns the aggregate `MuSigNoncePub`. This aggregation can be done before the message, or even the `KeySet` is known.
|
||||
|
||||
The functions for signing and verification are:
|
||||
|
||||
* `MuSigUtil.sign`
|
||||
* This function generates a MuSig partial signature using a private key and `MuSigNoncePriv`. This consists of a pair `(R, s)` where `R` is the aggregate nonce key (same for all signers) and `s` is the actual partial signature that needs to be shared.
|
||||
* `MuSigUtil.partialSigVerify`
|
||||
* This function validates a single partial signature against that signer's public key and `MuSigNoncePub`.
|
||||
* `MuSigUtil.signAgg`
|
||||
* This function aggregates all of the `s` values into a single valid `SchnorrDigitalSignature` (using the aggregate nonce key `R`).
|
||||
|
||||
Note that no new function is required for aggregate verification as `SchnorrPublicKey`'s `verify` function is to be used.
|
||||
|
||||
Lastly, it should be mentioned that `MuSigNoncePriv`s must be constructed using either `MuSigNoncePriv.gen` or `MuSigNoncePriv.genInternal` (the latter should only be used with 32 bytes of secure random entropy). These generation functions take as input any context information that is available at nonce generation time, namely your signing key, aggregate public key, message, and any extra bytes you may have available. Including these optional inputs improves the security of nonce generation (which must be absolutely secure).
|
||||
|
||||
The following code shows a two-party MuSig execution:
|
||||
|
||||
```scala mdoc:invisible
|
||||
import org.bitcoins.crypto.ECPrivateKey
|
||||
import org.bitcoins.crypto.musig._
|
||||
|
||||
val alicePrivKey = ECPrivateKey.freshPrivateKey
|
||||
val alicePubKey = alicePrivKey.schnorrPublicKey
|
||||
val bobPrivKey = ECPrivateKey.freshPrivateKey
|
||||
val bobPubKey = bobPrivKey.schnorrPublicKey
|
||||
val msg = scodec.bits.ByteVector.fill(32)(7)
|
||||
val tweaks = Vector.empty[MuSigTweak]
|
||||
```
|
||||
|
||||
```scala mdoc:compile-only
|
||||
// Alice and Bob generate and exchange nonce data (new nonces for every sig)
|
||||
val aliceNoncePriv = MuSigNoncePriv.gen()
|
||||
val aliceNonce = aliceNoncePriv.toPublicNonces // Alice sends this to Bob
|
||||
val bobNoncePriv = MuSigNoncePriv.gen()
|
||||
val bobNonce = bobNoncePriv.toPublicNonces // Bob sends this to Alice
|
||||
|
||||
// The aggregate musig nonce can be computed from Alice's and Bob's
|
||||
val aggMuSigNonce = MuSigNoncePub.aggregate(Vector(aliceNonce, bobNonce))
|
||||
|
||||
// Any party can (non-interactively) compute the aggregate public key
|
||||
val pubKeys = Vector(alicePubKey, bobPubKey)
|
||||
val keySet = KeySet(pubKeys, tweaks) // This is where you put MuSigTweaks
|
||||
val aggPubKey = keySet.aggPubKey.schnorrPublicKey
|
||||
|
||||
// Alice generates a partial signature for the message
|
||||
val (aliceR, aliceSig) =
|
||||
MuSigUtil.sign(aliceNoncePriv, aggMuSigNonce, alicePrivKey, msg, keySet)
|
||||
// Bob generates a partial signature for the message
|
||||
val (bobR, bobSig) =
|
||||
MuSigUtil.sign(bobNoncePriv, aggMuSigNonce, bobPrivKey, msg, keySet)
|
||||
require(aliceR == bobR)
|
||||
val R = aliceR
|
||||
|
||||
// Alice and Bob exchange and verify each other's sigs (s values)
|
||||
require(
|
||||
MuSigUtil.partialSigVerify(aliceSig,
|
||||
aliceNonce,
|
||||
aggMuSigNonce,
|
||||
alicePubKey,
|
||||
keySet,
|
||||
msg))
|
||||
require(
|
||||
MuSigUtil.partialSigVerify(bobSig,
|
||||
bobNonce,
|
||||
aggMuSigNonce,
|
||||
bobPubKey,
|
||||
keySet,
|
||||
msg))
|
||||
|
||||
// In the case that the aggregator is not Alice or Bob, R can be computed as follows
|
||||
val R2 = SigningSession(aggMuSigNonce, keySet, msg).aggNonce
|
||||
require(R2 == R)
|
||||
|
||||
// Finally, any party can aggregate the partial signatures
|
||||
val sig = MuSigUtil.signAgg(Vector(aliceSig, bobSig), R)
|
||||
|
||||
// This signature can now be validated as a normal BIP340 Schnorr signature
|
||||
require(aggPubKey.verify(msg, sig))
|
||||
```
|
||||
|
@ -74,6 +74,9 @@
|
||||
"crypto/crypto-intro": {
|
||||
"title": "Crypto Module"
|
||||
},
|
||||
"crypto/musig": {
|
||||
"title": "MuSig"
|
||||
},
|
||||
"crypto/sign": {
|
||||
"title": "Sign API"
|
||||
},
|
||||
|
@ -36,7 +36,8 @@
|
||||
"Crypto Module": [
|
||||
"crypto/crypto-intro",
|
||||
"crypto/sign",
|
||||
"crypto/adaptor-signatures"
|
||||
"crypto/adaptor-signatures",
|
||||
"crypto/musig"
|
||||
],
|
||||
"Fee Provider": [
|
||||
"fee-provider/fee-provider"
|
||||
|
Loading…
Reference in New Issue
Block a user