diff --git a/.travis.yml b/.travis.yml index 9d2e74ad51..8a09e39599 100644 --- a/.travis.yml +++ b/.travis.yml @@ -96,11 +96,11 @@ stages: - name: test if: commit_message !~ /^Docs:/ AND NOT - ((branch = master AND type = push) OR (tag IS present)) + ((branch = schnorr-dlc AND type = push) OR (tag IS present)) # don't run tests on merge builds, just publish library # and website - name: release - if: ((branch = master AND type = push) OR (tag IS present)) AND NOT fork + if: ((branch = schnorr-dlc AND type = push) OR (tag IS present)) AND NOT fork script: # Modify PATH to include binaries we are about to download diff --git a/core-test/src/test/resources/bip340-test-vectors.csv b/core-test/src/test/resources/bip340-test-vectors.csv new file mode 100644 index 0000000000..3d6f030eca --- /dev/null +++ b/core-test/src/test/resources/bip340-test-vectors.csv @@ -0,0 +1,16 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,067E337AD551B2276EC705E43F0920926A9CE08AC68159F9D258C9BBA412781C9F059FCDF4824F13B3D7C1305316F956704BB3FEA2C26142E18ACD90A90C947E,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0E12B8C520948A776753A96F21ABD7FDC2D7D0C0DDC90851BE17B04E75EF86A47EF0DA46C4DC4D0D1BCB8668C2CE16C54C7C23A6716EDE303AF86774917CF928,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,FC012F9FB8FE00A358F51EF93DCE0DC0C895F6E9A87C6C4905BC820B0C3677616B8737D14E703AF8E16E22E5B8F26227D41E5128F82D86F747244CC289C74D1D,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FC132D4E426DFF535AEC0FA7083AC5118BC1D5FFFD848ABD8290C23F271CA0DD11AEDCEA3F55DA9BD677FE29C9DDA0CF878BCE43FDE0E313D69D1AF7A5AE8369,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C630EC50E5363E227ACAC6F542CE1C0B186657E0E0D1A6FFE283A33438DE4738419,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,7036D6BFE1837AE919631039A2CF652A295DFAC9A8BBB0806014B2F48DD7C807941607B563ABBA414287F374A332BA3636DE009EE1EF551A17796B72B68B8A24,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F995A579DA959FA739FCE39E8BD16FECB5CDCF97060B2C73CDE60E87ABCA1AA5D9,FALSE,has_square_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,F8704654F4687B7365ED32E796DE92761390A3BCC495179BFE073817B7ED32824E76B987F7C1F9A751EF5C343F7645D3CFFC7D570B9A7192EBF1898E1344E3BF,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,7036D6BFE1837AE919631039A2CF652A295DFAC9A8BBB0806014B2F48DD7C8076BE9F84A9C5445BEBD780C8B5CCD45C883D0DC47CD594B21A858F31A19AAB71D,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000009915EE59F07F9DBBAEDC31BFCC9B34AD49DE669CD24773BCED77DDA36D073EC8,FALSE,sG - eP is infinite. Test fails in single verification if has_square_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000001C7EC918B2B9CF34071BB54BED7EB4BB6BAB148E9A7E36E6B228F95DFA08B43EC,FALSE,sG - eP is infinite. Test fails in single verification if has_square_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D941607B563ABBA414287F374A332BA3636DE009EE1EF551A17796B72B68B8A24,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F941607B563ABBA414287F374A332BA3636DE009EE1EF551A17796B72B68B8A24,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,7036D6BFE1837AE919631039A2CF652A295DFAC9A8BBB0806014B2F48DD7C807FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,7036D6BFE1837AE919631039A2CF652A295DFAC9A8BBB0806014B2F48DD7C807941607B563ABBA414287F374A332BA3636DE009EE1EF551A17796B72B68B8A24,FALSE,public key is not a valid X coordinate because it exceeds the field size \ No newline at end of file diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/BIP340Test.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/BIP340Test.scala new file mode 100644 index 0000000000..155d392a45 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/BIP340Test.scala @@ -0,0 +1,119 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.testkit.util.BitcoinSUnitTest +import org.scalatest.Assertion +import scodec.bits.ByteVector + +import scala.util.{Failure, Success, Try} + +/** Tests from https://github.com/sipa/bips/blob/bip-taproot/bip-0340/test-vectors.csv */ +class BIP340Test extends BitcoinSUnitTest { + behavior of "Schnorr Signing" + + def testSign( + index: Int, + secKey: ECPrivateKey, + auxRand: ByteVector, + msg: ByteVector, + expectedSig: SchnorrDigitalSignature): Assertion = { + val secpSig = secKey.schnorrSign(msg, auxRand) + val bouncyCastleSig = BouncyCastleUtil.schnorrSign(msg, secKey, auxRand) + assert(secpSig == expectedSig, + s"Test $index failed signing for libsecp256k1") + assert(bouncyCastleSig == expectedSig, + s"Test $index failed signing for Bouncy Castle") + assert(bouncyCastleSig == secpSig) + } + + def testVerify( + index: Int, + pubKey: SchnorrPublicKey, + msg: ByteVector, + sig: SchnorrDigitalSignature, + expectedResult: Boolean, + comment: String): Assertion = { + val secpResult = pubKey.verify(msg, sig) + val bouncyCastleResult = BouncyCastleUtil.schnorrVerify(msg, pubKey, sig) + assert(secpResult == expectedResult, + s"Test $index failed verification for libsecp256k1: $comment") + assert(bouncyCastleResult == expectedResult, + s"Test $index failed verification for Bouncy Castle: $comment") + assert(bouncyCastleResult == secpResult) + } + + def test( + index: Int, + secKeyOpt: Option[String], + pubKey: String, + auxRandOpt: Option[String], + msg: String, + sig: String, + result: Boolean, + comment: String): Assertion = { + val pkT = Try(SchnorrPublicKey(pubKey)) + val msgBytes = ByteVector.fromHex(msg).get + val schnorrSigT = Try(SchnorrDigitalSignature(sig)) + + (pkT, schnorrSigT) match { + case (Success(pk), Success(schnorrSig)) => + (secKeyOpt, auxRandOpt) match { + case (Some(secKeyStr), Some(auxRandStr)) => + val secKey = ECPrivateKey(secKeyStr) + assert(secKey.schnorrPublicKey == pk) + val auxRand = ByteVector.fromHex(auxRandStr).get + testSign(index, secKey, auxRand, msgBytes, schnorrSig) + case _ => () + } + + testVerify(index, pk, msgBytes, schnorrSig, result, comment) + case (Failure(_), _) | + (_, Failure(_)) => // Must be verify only test resulting in false + assert(secKeyOpt.isEmpty) + assert(auxRandOpt.isEmpty) + assert(!result, s"Test $index failed to parse signature: $comment") + } + } + + private def toOpt(str: String): Option[String] = { + if (str.isEmpty) { + None + } else { + Some(str) + } + } + + it must "pass the BIP 340 test-vectors with both secp256k1 bindings and bouncy castle" in { + val bufferedSource = + io.Source.fromURL(getClass.getResource("/bip340-test-vectors.csv")) + try { + val lines = bufferedSource.getLines + val _ = lines.next() + for (line <- bufferedSource.getLines) { + val testVec = line.split(",").map(_.trim) + val index = testVec.head.toInt + val secKeyOpt = toOpt(testVec(1)) + val pubKey = testVec(2) + val auxRandOpt = toOpt(testVec(3)) + val msg = testVec(4) + val sig = testVec(5) + val result = testVec(6).toBoolean + val comment = if (testVec.length > 7) { + testVec(7) + } else { + "" + } + + test(index = index, + secKeyOpt = secKeyOpt, + pubKey = pubKey, + auxRandOpt = auxRandOpt, + msg = msg, + sig = sig, + result = result, + comment = comment) + } + } finally { + bufferedSource.close() + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/BouncyCastleSecp256k1Test.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/BouncyCastleSecp256k1Test.scala index a4525ee8ba..6269d131a1 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/BouncyCastleSecp256k1Test.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/BouncyCastleSecp256k1Test.scala @@ -9,6 +9,9 @@ import scodec.bits.ByteVector class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + behavior of "CryptoLibraries" override def withFixture(test: NoArgTest): Outcome = { @@ -23,16 +26,7 @@ class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { it must "add private keys the same" in { forAll(CryptoGenerators.privateKey, CryptoGenerators.privateKey) { case (priv1, priv2) => - val sumWithBouncyCastle = - BouncyCastleUtil.addNumbers(priv1.bytes, priv2.bytes) - val sumWithSecp = NativeSecp256k1.privKeyTweakAdd(priv1.bytes.toArray, - priv2.bytes.toArray) - - val sumKeyWithBouncyCastle = - ECPrivateKey(ByteVector(sumWithBouncyCastle.toByteArray)) - val sumKeyWithSecp = ECPrivateKey(ByteVector(sumWithSecp)) - - assert(sumKeyWithBouncyCastle == sumKeyWithSecp) + assert(priv1.addWithBouncyCastle(priv2) == priv1.addWithSecp(priv2)) } } @@ -50,6 +44,15 @@ class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { } } + it must "multiply keys the same" in { + forAll(CryptoGenerators.publicKey, CryptoGenerators.fieldElement) { + case (pubKey, tweak) => + assert( + pubKey.tweakMultiplyWithSecp(tweak) == pubKey + .tweakMultiplyWithBouncyCastle(tweak)) + } + } + it must "validate keys the same" in { val keyOrGarbageGen = Gen.oneOf(CryptoGenerators.publicKey.map(_.bytes), NumberGenerator.bytevector(33)) @@ -100,4 +103,63 @@ class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { .verify(bytes, badSig, useSecp = true)) } } + + it must "compute schnorr signatures the same" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, bytes, auxRand) => + assert( + privKey.schnorrSign(bytes, auxRand, useSecp = false) == privKey + .schnorrSign(bytes, auxRand, useSecp = true)) + } + } + + it must "compute schnorr signature for fixed nonce the same" in { + forAll(CryptoGenerators.privateKey, + CryptoGenerators.privateKey, + NumberGenerator.bytevector(32)) { + case (privKey, nonceKey, bytes) => + val sigBC = privKey + .schnorrSignWithNonce(bytes, nonceKey, useSecp = false) + val sigSecP = privKey + .schnorrSignWithNonce(bytes, nonceKey, useSecp = true) + assert(sigBC.bytes == sigSecP.bytes) + } + } + + it must "validate schnorr signatures the same" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + CryptoGenerators.schnorrDigitalSignature) { + case (privKey, bytes, badSig) => + val sig = privKey.schnorrSign(bytes) + val pubKey = privKey.schnorrPublicKey + assert( + pubKey.verify(bytes, sig, useSecp = false) == pubKey + .verify(bytes, sig, useSecp = true)) + assert( + pubKey.verify(bytes, badSig, useSecp = false) == pubKey + .verify(bytes, badSig, useSecp = true)) + } + } + + it must "compute schnorr signature points the same" in { + forAll(CryptoGenerators.schnorrPublicKey, + CryptoGenerators.schnorrNonce, + NumberGenerator.bytevector(32)) { + case (pubKey, nonce, bytes) => + val bouncyCastleSigPoint = pubKey.computeSigPoint(bytes, + nonce, + compressed = true, + useSecp = false) + + val secpSigPoint = pubKey.computeSigPoint(bytes, + nonce, + compressed = true, + useSecp = true) + + assert(bouncyCastleSigPoint == secpSigPoint) + } + } } diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/ECPrivateKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/ECPrivateKeyTest.scala index 68fc2219a8..63ae88cf40 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/ECPrivateKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/ECPrivateKeyTest.scala @@ -84,6 +84,7 @@ class ECPrivateKeyTest extends BitcoinSUnitTest { it must "have serialization symmetry" in { forAll(CryptoGenerators.privateKey) { privKey => assert(ECPrivateKey(privKey.hex) == privKey) + assert(ECPrivateKey.fromFieldElement(privKey.fieldElement) == privKey) } } @@ -108,4 +109,16 @@ class ECPrivateKeyTest extends BitcoinSUnitTest { ECPrivateKey().toString must be("Masked(ECPrivateKeyImpl)") } + it must "successfully negate itself" in { + forAll(CryptoGenerators.nonZeroPrivKey) { privKey => + val negPrivKey = privKey.negate + val pubKey = privKey.publicKey + val negPubKey = negPrivKey.publicKey + assert(pubKey.bytes.tail == negPubKey.bytes.tail) + assert(pubKey.bytes.head != negPubKey.bytes.head) + assert( + privKey.fieldElement.add(negPrivKey.fieldElement) == FieldElement.zero) + } + } + } diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/FieldElementTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/FieldElementTest.scala new file mode 100644 index 0000000000..8996d7276d --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/FieldElementTest.scala @@ -0,0 +1,97 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class FieldElementTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "FieldElement" + + private val N = CryptoParams.curve.getN + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.fieldElement) { fe => + assert(FieldElement(fe.bytes) == fe) + } + } + + it must "add zero correctly" in { + forAll(CryptoGenerators.fieldElement) { fe => + assert(fe.add(FieldElement.zero) == fe) + assert(FieldElement.zero.add(fe) == fe) + } + } + + it must "add small numbers correctly" in { + forAll(CryptoGenerators.smallFieldElement, + CryptoGenerators.smallFieldElement) { + case (fe1, fe2) => + val feSum = fe1.add(fe2).toBigInteger + val bigIntSum = fe1.toBigInteger.add(fe2.toBigInteger) + + assert(feSum == bigIntSum) + } + } + + it must "add large numbers correctly" in { + forAll(CryptoGenerators.largeFieldElement, + CryptoGenerators.largeFieldElement) { + case (fe1, fe2) => + val feSum = fe1.add(fe2).toBigInteger + val bigIntSum = fe1.toBigInteger.add(fe2.toBigInteger).subtract(N) + + assert(feSum == bigIntSum) + } + } + + it must "subtract numbers correctly" in { + forAll(CryptoGenerators.fieldElement, CryptoGenerators.fieldElement) { + case (fe1, fe2) => + if (fe1.toBigInteger.compareTo(fe2.toBigInteger) > 0) { + val feDiff = fe1.subtract(fe2).toBigInteger + val bigIntDiff = fe1.toBigInteger.subtract(fe2.toBigInteger) + + assert(feDiff == bigIntDiff) + } else { + val feDiff = fe2.subtract(fe1).toBigInteger + val bigIntDiff = fe2.toBigInteger.subtract(fe1.toBigInteger) + + assert(feDiff == bigIntDiff) + } + } + } + + it must "wrap around correctly" in { + assert(FieldElement.nMinusOne.add(FieldElement.one) == FieldElement.zero) + assert( + FieldElement.zero.subtract(FieldElement.one) == FieldElement.nMinusOne) + } + + it must "multiply small numbers correctly" in { + forAll(CryptoGenerators.reallySmallFieldElement, + CryptoGenerators.reallySmallFieldElement) { + case (fe1, fe2) => + val feProduct = fe1.multiply(fe2).toBigInteger + val bigIntProduct = fe1.toBigInteger.multiply(fe2.toBigInteger) + + assert(feProduct == bigIntProduct) + } + } + + it must "negate correctly" in { + forAll(CryptoGenerators.fieldElement) { fe => + val negFe = fe.negate + assert(fe.add(negFe) == FieldElement.zero) + } + } + + it must "invert correctly" in { + forAll(CryptoGenerators.fieldElement) { fe => + val feInv = fe.inverse + assert(fe.multiply(feInv) == FieldElement.one) + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrDigitalSignatureTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrDigitalSignatureTest.scala new file mode 100644 index 0000000000..d3ca2b6a17 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrDigitalSignatureTest.scala @@ -0,0 +1,132 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.core.util.{CryptoUtil, NumberUtil} +import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator} +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class SchnorrDigitalSignatureTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "SchnorrDigitalSignature" + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.schnorrDigitalSignature) { sig => + assert(SchnorrDigitalSignature(sig.bytes) == sig) + } + } + + it must "must create and verify a digital signature" in { + forAll(NumberGenerator.bytevector(32), CryptoGenerators.privateKey) { + case (bytes, privKey) => + val sig = privKey.schnorrSign(bytes) + assert(privKey.publicKey.schnorrVerify(bytes, sig)) + } + } + + it must "must not reuse R values" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, bytes1, bytes2) => + val sig1 = privKey.schnorrSign(bytes1) + val sig2 = privKey.schnorrSign(bytes2) + assert(sig1.bytes != sig2.bytes) + assert(sig1.rx != sig2.rx) + } + } + + it must "generate R values correctly" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, auxRand, bytes) => + val nonce = SchnorrNonce.kFromBipSchnorr(privKey, bytes, auxRand) + + val sig1 = privKey.schnorrSign(bytes, auxRand) + val sig2 = privKey.schnorrSignWithNonce(bytes, nonce) + + assert(sig1 == sig2) + } + } + + it must "correctly compute signature points" in { + forAll(CryptoGenerators.privateKey, NumberGenerator.bytevector(32)) { + case (privKey, data) => + val pubKey = privKey.publicKey + val sig = privKey.schnorrSign(data) + + val sigPoint = pubKey.schnorrComputePoint(data, sig.rx) + assert(sigPoint == sig.sig.toPrivateKey.publicKey) + } + } + + it must "correctly compute signature points for sigs with fixed nonces" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + CryptoGenerators.privateKey) { + case (privKey, data, nonce) => + val pubKey = privKey.publicKey + val sig = privKey.schnorrSignWithNonce(data, nonce) + assert(sig.rx == nonce.schnorrNonce) + + val sigPoint = pubKey.schnorrComputePoint(data, sig.rx) + assert(sigPoint == sig.sig.toPrivateKey.publicKey) + } + } + + /** Schnorr signatures have the property that if two messages are signed with the same key + * and nonce, then they are leaked: + * + * sig1 = nonce + message1*privKey + * sig2 = nonce + message2*privKey + * + * => sig1 - sig2 = (message1 - message2)*privKey + * => privKey = (sig1 - sig2) * inverse(message1 - message2) + */ + it must "leak keys if two messages are signed" in { + forAll(CryptoGenerators.nonZeroPrivKey, + CryptoGenerators.nonZeroPrivKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, nonce, message1, message2) => + // This will only work if we sign two different messages + assert(message1 != message2) + + // Sign both messages using the same privKey and nonce + val sig1 = privKey.schnorrSignWithNonce(message1, nonce) + val sig2 = privKey.schnorrSignWithNonce(message2, nonce) + + // s1 = nonce + e1*privKey + val s1 = sig1.sig + // s2 = nonce + e2*privKey + val s2 = sig2.sig + + // When signing a message you actually sign SHA256_challenge(Rx || pubKey || message) + val bytesToHash1 = sig1.rx.bytes ++ privKey.schnorrPublicKey.bytes ++ message1 + val e1Bytes = CryptoUtil.sha256SchnorrChallenge(bytesToHash1).bytes + + val bytesToHash2 = sig2.rx.bytes ++ privKey.schnorrPublicKey.bytes ++ message2 + val e2Bytes = CryptoUtil.sha256SchnorrChallenge(bytesToHash2).bytes + + val e1 = NumberUtil.uintToFieldElement(e1Bytes) + val e2 = NumberUtil.uintToFieldElement(e2Bytes) + + val k = nonce.nonceKey.fieldElement + val x = privKey.schnorrKey.fieldElement + + // Test that we have correctly computed the components + assert(k.add(e1.multiply(x)) == s1) + assert(k.add(e2.multiply(x)) == s2) + + // Note that all of s1, s2, e1, and e2 are public information: + // s1 - s2 = nonce + e1*privKey - (nonce + e2*privKey) = privKey*(e1-e2) + // => privKey = (s1 - s2) * modInverse(e1 - e2) + val privNum = s1.subtract(s2).multInv(e1.subtract(e2)) + + // Assert that we've correctly recovered the private key form public info + assert(privNum == x) + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrNonceTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrNonceTest.scala new file mode 100644 index 0000000000..219b518479 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrNonceTest.scala @@ -0,0 +1,39 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class SchnorrNonceTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "SchnorrNonce" + + it must "fail for incorrect lengths" in { + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a")) + + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a010203")) + } + + it must "fail for invalid x coordinate" in { + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34")) + + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30")) + } + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.schnorrNonce) { pubKey => + assert(SchnorrNonce(pubKey.bytes) == pubKey) + assert(SchnorrNonce(pubKey.xCoord) == pubKey) + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrPublicKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrPublicKeyTest.scala new file mode 100644 index 0000000000..1e0e0e6857 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/SchnorrPublicKeyTest.scala @@ -0,0 +1,39 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class SchnorrPublicKeyTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "SchnorrPublicKey" + + it must "fail for incorrect lengths" in { + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a")) + + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a010203")) + } + + it must "fail for invalid x coordinate" in { + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34")) + + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30")) + } + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.schnorrPublicKey) { pubKey => + assert(SchnorrPublicKey(pubKey.bytes) == pubKey) + assert(SchnorrPublicKey(pubKey.xCoord) == pubKey) + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala b/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala index 427eba9663..2205909483 100644 --- a/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala @@ -1,6 +1,6 @@ package org.bitcoins.core.util -import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator} import org.bitcoins.testkit.util.BitcoinSUnitTest import scodec.bits._ @@ -92,4 +92,18 @@ class CryptoUtilTest extends BitcoinSUnitTest { } } + it must "compute tagged hashes correctly" in { + forAll(NumberGenerator.bytevector) { bytes => + assert( + CryptoUtil.sha256SchnorrChallenge(bytes) == CryptoUtil + .taggedSha256(bytes, "BIP340/challenge")) + assert( + CryptoUtil.sha256SchnorrNonce(bytes) == CryptoUtil + .taggedSha256(bytes, "BIP340/nonce")) + assert( + CryptoUtil.sha256SchnorrAuxRand(bytes) == CryptoUtil + .taggedSha256(bytes, "BIP340/aux")) + } + } + } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/BouncyCastleUtil.scala b/core/src/main/scala/org/bitcoins/core/crypto/BouncyCastleUtil.scala index 8e30f3bacc..2859a73edd 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/BouncyCastleUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/BouncyCastleUtil.scala @@ -2,7 +2,7 @@ package org.bitcoins.core.crypto import java.math.BigInteger -import org.bitcoins.core.util.BitcoinSUtil +import org.bitcoins.core.util.{BitcoinSUtil, CryptoUtil} import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.params.{ ECPrivateKeyParameters, @@ -12,23 +12,20 @@ import org.bouncycastle.crypto.signers.{ECDSASigner, HMacDSAKCalculator} import org.bouncycastle.math.ec.{ECCurve, ECPoint} import scodec.bits.ByteVector -import scala.util.Try +import scala.util.{Failure, Success, Try} object BouncyCastleUtil { private val curve: ECCurve = CryptoParams.curve.getCurve private val G: ECPoint = CryptoParams.curve.getG - private val N: BigInteger = CryptoParams.curve.getN private def getBigInteger(bytes: ByteVector): BigInteger = { new BigInteger(1, bytes.toArray) } - def addNumbers(num1: ByteVector, num2: ByteVector): BigInteger = { - val bigInteger1 = getBigInteger(num1) - val bigInteger2 = getBigInteger(num2) - - bigInteger1.add(bigInteger2).mod(N) + def pubKeyTweakMul(publicKey: ECPublicKey, tweak: ByteVector): ECPublicKey = { + val point = publicKey.toPoint.multiply(getBigInteger(tweak)) + ECPublicKey.fromPoint(point, publicKey.isCompressed) } def decodePoint(bytes: ByteVector): ECPoint = { @@ -111,4 +108,79 @@ object BouncyCastleUtil { } resultTry.getOrElse(false) } + + def schnorrSign( + dataToSign: ByteVector, + privateKey: ECPrivateKey, + auxRand: ByteVector): SchnorrDigitalSignature = { + val nonceKey = + SchnorrNonce.kFromBipSchnorr(privateKey, dataToSign, auxRand) + + schnorrSignWithNonce(dataToSign, privateKey, nonceKey) + } + + def schnorrSignWithNonce( + dataToSign: ByteVector, + privateKey: ECPrivateKey, + nonceKey: ECPrivateKey): SchnorrDigitalSignature = { + val rx = nonceKey.schnorrNonce + val k = nonceKey.nonceKey.fieldElement + val x = privateKey.schnorrKey.fieldElement + val e = CryptoUtil + .sha256SchnorrChallenge( + rx.bytes ++ privateKey.schnorrPublicKey.bytes ++ dataToSign) + .bytes + + val challenge = x.multiply(FieldElement(e)) + val sig = k.add(challenge) + + SchnorrDigitalSignature(rx, sig) + } + + def schnorrVerify( + data: ByteVector, + schnorrPubKey: SchnorrPublicKey, + signature: SchnorrDigitalSignature): Boolean = { + val rx = signature.rx + val sT = Try(signature.sig.toPrivateKey) + + sT match { + case Success(s) => + val eBytes = CryptoUtil + .sha256SchnorrChallenge(rx.bytes ++ schnorrPubKey.bytes ++ data) + .bytes + + val e = FieldElement(eBytes) + val negE = e.negate + + val sigPoint = s.publicKey + val challengePoint = schnorrPubKey.publicKey.tweakMultiply(negE) + val computedR = challengePoint.add(sigPoint) + val yCoord = computedR.toPoint.getRawYCoord + + yCoord != null && yCoord.sqrt() != null && computedR.schnorrNonce == rx + case Failure(_) => false + } + } + + def schnorrComputeSigPoint( + data: ByteVector, + nonce: SchnorrNonce, + pubKey: SchnorrPublicKey, + compressed: Boolean): ECPublicKey = { + val eBytes = CryptoUtil + .sha256SchnorrChallenge(nonce.bytes ++ pubKey.bytes ++ data) + .bytes + + val e = FieldElement(eBytes) + + val compressedSigPoint = + nonce.publicKey.add(pubKey.publicKey.tweakMultiply(e)) + + if (compressed) { + compressedSigPoint + } else { + compressedSigPoint.decompressed + } + } } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala b/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala index bc3da2d446..572107b003 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/ECKey.scala @@ -74,6 +74,127 @@ sealed abstract class ECPrivateKey implicit ec: ExecutionContext): Future[ECDigitalSignature] = Future(sign(hash)) + def schnorrSign(dataToSign: ByteVector): SchnorrDigitalSignature = { + schnorrSign(dataToSign, Secp256k1Context.isEnabled) + } + + def schnorrSign( + dataToSign: ByteVector, + useSecp: Boolean): SchnorrDigitalSignature = { + val auxRand = ECPrivateKey.freshPrivateKey.bytes + schnorrSign(dataToSign, auxRand, useSecp) + } + + def schnorrSign( + dataToSign: ByteVector, + auxRand: ByteVector): SchnorrDigitalSignature = { + schnorrSign(dataToSign, auxRand, Secp256k1Context.isEnabled) + } + + def schnorrSign( + dataToSign: ByteVector, + auxRand: ByteVector, + useSecp: Boolean): SchnorrDigitalSignature = { + if (useSecp) { + schnorrSignWithSecp(dataToSign, auxRand) + } else { + schnorrSignWithBouncyCastle(dataToSign, auxRand) + } + } + + def schnorrSignWithSecp( + dataToSign: ByteVector, + auxRand: ByteVector): SchnorrDigitalSignature = { + val sigBytes = + NativeSecp256k1.schnorrSign(dataToSign.toArray, + bytes.toArray, + auxRand.toArray) + SchnorrDigitalSignature(ByteVector(sigBytes)) + } + + def schnorrSignWithBouncyCastle( + dataToSign: ByteVector, + auxRand: ByteVector): SchnorrDigitalSignature = { + BouncyCastleUtil.schnorrSign(dataToSign, this, auxRand) + } + + def schnorrSignWithNonce( + dataToSign: ByteVector, + nonce: ECPrivateKey): SchnorrDigitalSignature = { + schnorrSignWithNonce(dataToSign, nonce, Secp256k1Context.isEnabled) + } + + def schnorrSignWithNonce( + dataToSign: ByteVector, + nonce: ECPrivateKey, + useSecp: Boolean): SchnorrDigitalSignature = { + if (useSecp) { + schnorrSignWithNonceWithSecp(dataToSign, nonce) + } else { + schnorrSignWithNonceWithBouncyCastle(dataToSign, nonce) + } + } + + def schnorrSignWithNonceWithSecp( + dataToSign: ByteVector, + nonce: ECPrivateKey): SchnorrDigitalSignature = { + val sigBytes = + NativeSecp256k1.schnorrSignWithNonce(dataToSign.toArray, + bytes.toArray, + nonce.bytes.toArray) + SchnorrDigitalSignature(ByteVector(sigBytes)) + } + + def schnorrSignWithNonceWithBouncyCastle( + dataToSign: ByteVector, + nonce: ECPrivateKey): SchnorrDigitalSignature = { + BouncyCastleUtil.schnorrSignWithNonce(dataToSign, this, nonce) + } + + def nonceKey: ECPrivateKey = { + if (schnorrNonce.publicKey == publicKey) { + this + } else { + this.negate + } + } + + def schnorrKey: ECPrivateKey = { + if (schnorrPublicKey.publicKey == publicKey) { + this + } else { + this.negate + } + } + + def negate: ECPrivateKey = { + val negPrivKeyNum = CryptoParams.curve.getN + .subtract(new BigInteger(1, bytes.toArray)) + ECPrivateKey(ByteVector(negPrivKeyNum.toByteArray)) + } + + def add(other: ECPrivateKey): ECPrivateKey = { + add(other, Secp256k1Context.isEnabled) + } + + def add(other: ECPrivateKey, useSecp: Boolean): ECPrivateKey = { + if (useSecp) { + addWithSecp(other) + } else { + addWithBouncyCastle(other) + } + } + + def addWithSecp(other: ECPrivateKey): ECPrivateKey = { + val sumBytes = + NativeSecp256k1.privKeyTweakAdd(bytes.toArray, other.bytes.toArray) + ECPrivateKey(ByteVector(sumBytes)) + } + + def addWithBouncyCastle(other: ECPrivateKey): ECPrivateKey = { + fieldElement.add(other.fieldElement).toPrivateKey + } + /** Signifies if the this private key corresponds to a compressed public key */ def isCompressed: Boolean @@ -103,6 +224,14 @@ sealed abstract class ECPrivateKey BouncyCastleUtil.computePublicKey(this) } + def schnorrPublicKey: SchnorrPublicKey = { + SchnorrPublicKey(publicKey.bytes) + } + + def schnorrNonce: SchnorrNonce = { + SchnorrNonce(publicKey.bytes) + } + /** * Converts a [[org.bitcoins.core.crypto.ECPrivateKey ECPrivateKey]] to * [[https://en.bitcoin.it/wiki/Wallet_import_format WIF]] @@ -119,6 +248,8 @@ sealed abstract class ECPrivateKey Base58.encode(encodedPrivKey) } + def fieldElement: FieldElement = FieldElement(bytes) + override def toStringSensitive: String = s"ECPrivateKey($hex,$isCompressed)" } @@ -167,6 +298,10 @@ object ECPrivateKey extends Factory[ECPrivateKey] { def fromHex(hex: String, isCompressed: Boolean): ECPrivateKey = fromBytes(BitcoinSUtil.decodeHex(hex), isCompressed) + def fromFieldElement(fieldElement: FieldElement): ECPrivateKey = { + fieldElement.toPrivateKey + } + /** Generates a fresh [[org.bitcoins.core.crypto.ECPrivateKey ECPrivateKey]] that has not been used before. */ def apply(): ECPrivateKey = ECPrivateKey(true) @@ -327,6 +462,23 @@ sealed abstract class ECPublicKey extends BaseECKey { def verify(hex: String, signature: ECDigitalSignature): Boolean = verify(BitcoinSUtil.decodeHex(hex), signature) + def schnorrVerify( + data: ByteVector, + signature: SchnorrDigitalSignature): Boolean = { + schnorrPublicKey.verify(data, signature) + } + + def schnorrComputePoint( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean = isCompressed): ECPublicKey = { + schnorrPublicKey.computeSigPoint(data, nonce, compressed) + } + + def schnorrPublicKey: SchnorrPublicKey = SchnorrPublicKey(bytes) + + def schnorrNonce: SchnorrNonce = SchnorrNonce(bytes) + override def toString = "ECPublicKey(" + hex + ")" /** Checks if the [[org.bitcoins.core.crypto.ECPublicKey ECPublicKey]] is compressed */ @@ -380,6 +532,29 @@ sealed abstract class ECPublicKey extends BaseECKey { ECPublicKey.fromPoint(sumPoint) } + + def tweakMultiply(tweak: FieldElement): ECPublicKey = { + tweakMultiply(tweak, Secp256k1Context.isEnabled) + } + + def tweakMultiply(tweak: FieldElement, useSecp: Boolean): ECPublicKey = { + if (useSecp) { + tweakMultiplyWithSecp(tweak) + } else { + tweakMultiplyWithBouncyCastle(tweak) + } + } + + def tweakMultiplyWithSecp(tweak: FieldElement): ECPublicKey = { + val mulBytes = NativeSecp256k1.pubKeyTweakMul(bytes.toArray, + tweak.bytes.toArray, + isCompressed) + ECPublicKey(ByteVector(mulBytes)) + } + + def tweakMultiplyWithBouncyCastle(tweak: FieldElement): ECPublicKey = { + BouncyCastleUtil.pubKeyTweakMul(this, tweak.bytes) + } } object ECPublicKey extends Factory[ECPublicKey] { diff --git a/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala b/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala index 1e03149c1f..b482394b17 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala @@ -201,12 +201,14 @@ sealed abstract class ExtPrivateKey //should be ECGroup addition //parse256(IL) + kpar (mod n) val tweak = if (Secp256k1Context.isEnabled) { - NativeSecp256k1.privKeyTweakAdd(il.toArray, key.bytes.toArray) + val tweakByteArr = + NativeSecp256k1.privKeyTweakAdd(il.toArray, key.bytes.toArray) + ByteVector(tweakByteArr) } else { - val sum = BouncyCastleUtil.addNumbers(key.bytes, il) - sum.toByteArray + val sum = key.fieldElement.add(FieldElement(il)) + sum.bytes } - val childKey = ECPrivateKey(ByteVector(tweak)) + val childKey = ECPrivateKey(tweak) val fp = CryptoUtil.sha256Hash160(key.publicKey.bytes).bytes.take(4) ExtPrivateKey(version, depth + UInt8.one, fp, idx, ChainCode(ir), childKey) } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/FieldElement.scala b/core/src/main/scala/org/bitcoins/core/crypto/FieldElement.scala new file mode 100644 index 0000000000..5120908760 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/crypto/FieldElement.scala @@ -0,0 +1,134 @@ +package org.bitcoins.core.crypto + +import java.math.BigInteger + +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.Factory +import org.bouncycastle.math.ec.ECPoint +import scodec.bits.ByteVector + +import scala.util.Try + +/** + * Represents integers modulo the secp256k1 field size: pow(2,256) - 0x1000003D1. + * + * Supports arithmetic for these elements including +, -, *, and inverses. + * Supports 32 byte serialization as is needed for ECPrivateKeys. + */ +case class FieldElement(bytes: ByteVector) extends NetworkElement { + require(bytes.length == 32, s"Field elements must have 32 bytes, got $bytes") + + private val privKeyT: Try[ECPrivateKey] = Try(ECPrivateKey(bytes)) + + require( + privKeyT.isSuccess || isZero, + s"$bytes is not a valid field element: ${privKeyT.failed.get.getMessage}") + + def isZero: Boolean = bytes.toArray.forall(_ == 0.toByte) + + def toPrivateKey: ECPrivateKey = + if (!isZero) { + privKeyT.get + } else { + throw new RuntimeException("Cannot turn zero into a private key") + } + + def toBigInteger: BigInteger = FieldElement.getBigInteger(bytes) + + def add(other: FieldElement): FieldElement = { + FieldElement.add(this, other) + } + + def subtract(other: FieldElement): FieldElement = { + add(other.negate) + } + + def multiply(other: FieldElement): FieldElement = { + FieldElement.multiply(this, other) + } + + def multInv(other: FieldElement): FieldElement = { + multiply(other.inverse) + } + + def negate: FieldElement = { + FieldElement.negate(this) + } + + def getPoint: ECPoint = FieldElement.computePoint(this) + + def getPublicKey: ECPublicKey = toPrivateKey.publicKey + + def inverse: FieldElement = FieldElement.computeInverse(this) +} + +object FieldElement extends Factory[FieldElement] { + override def fromBytes(bytes: ByteVector): FieldElement = { + if (bytes.length < 32) { + new FieldElement(bytes.padLeft(32)) + } else if (bytes.length == 32) { + new FieldElement(bytes) + } else if (bytes.length == 33 && bytes.head == 0.toByte) { + new FieldElement(bytes.tail) + } else { + throw new IllegalArgumentException( + s"Field element cannot have more than 32 bytes, got $bytes") + } + } + + def apply(num: BigInt): FieldElement = { + FieldElement(num.underlying()) + } + + def apply(num: BigInteger): FieldElement = { + FieldElement.fromByteArray(num.mod(N).toByteArray) + } + + def fromByteArray(bytes: Array[Byte]): FieldElement = { + FieldElement(ByteVector(bytes)) + } + + val zero: FieldElement = FieldElement(ByteVector.empty) + val one: FieldElement = FieldElement(ByteVector.fromByte(1)) + + val nMinusOne: FieldElement = FieldElement( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140") + + private val G: ECPoint = CryptoParams.curve.getG + private val N: BigInteger = CryptoParams.curve.getN + + private def getBigInteger(bytes: ByteVector): BigInteger = { + new BigInteger(1, bytes.toArray) + } + + def add(fe1: FieldElement, fe2: FieldElement): FieldElement = { + val num1 = fe1.toBigInteger + val num2 = fe2.toBigInteger + + val sum = num1.add(num2).mod(N) + FieldElement(sum) + } + + def multiply(fe1: FieldElement, fe2: FieldElement): FieldElement = { + val num1 = fe1.toBigInteger + val num2 = fe2.toBigInteger + + val sum = num1.multiply(num2).mod(N) + FieldElement(sum) + } + + def negate(fe: FieldElement): FieldElement = { + val neg = N.subtract(fe.toBigInteger) + FieldElement(neg) + } + + def computePoint(fe: FieldElement): ECPoint = G.multiply(fe.toBigInteger) + + /** Computes the inverse (mod M) of the input using the Euclidean Algorithm (log time) + * Cribbed from [[https://www.geeksforgeeks.org/multiplicative-inverse-under-modulo-m/]] + */ + def computeInverse(fe: FieldElement): FieldElement = { + val inv = fe.toBigInteger.modInverse(N) + FieldElement(inv) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/crypto/SchnorrDigitalSignature.scala b/core/src/main/scala/org/bitcoins/core/crypto/SchnorrDigitalSignature.scala new file mode 100644 index 0000000000..e12f18302d --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/crypto/SchnorrDigitalSignature.scala @@ -0,0 +1,19 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.Factory +import scodec.bits.ByteVector + +case class SchnorrDigitalSignature(rx: SchnorrNonce, sig: FieldElement) + extends NetworkElement { + override def bytes: ByteVector = rx.bytes ++ sig.bytes +} + +object SchnorrDigitalSignature extends Factory[SchnorrDigitalSignature] { + override def fromBytes(bytes: ByteVector): SchnorrDigitalSignature = { + require(bytes.length == 64, + s"SchnorrDigitalSignature must be exactly 64 bytes, got $bytes") + SchnorrDigitalSignature(SchnorrNonce(bytes.take(32)), + FieldElement(bytes.drop(32))) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/crypto/SchnorrNonce.scala b/core/src/main/scala/org/bitcoins/core/crypto/SchnorrNonce.scala new file mode 100644 index 0000000000..55b7871b36 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/crypto/SchnorrNonce.scala @@ -0,0 +1,91 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.{BitcoinSUtil, CryptoUtil, Factory} +import scodec.bits.ByteVector + +import scala.annotation.tailrec +import scala.util.Try + +case class SchnorrNonce(bytes: ByteVector) extends NetworkElement { + require(bytes.length == 32, s"Schnorr nonce must be 32 bytes, get $bytes") + + private val evenKey: ECPublicKey = ECPublicKey(s"02$hex") + private val oddKey: ECPublicKey = ECPublicKey(s"03$hex") + + private val yCoordEven: Boolean = { + evenKey.toPoint.getRawYCoord.sqrt() != null + } + + /** Computes the public key associated with a SchnorrNonce as specified in bip-schnorr. + * They y-coordinate is chosen to be a quadratic residue. + */ + val publicKey: ECPublicKey = { + if (yCoordEven) { + evenKey + } else { + oddKey + } + } + + require(Try(publicKey).isSuccess, + s"Schnorr nonce must be a valid x coordinate, got $bytes") + require( + publicKey.toPoint.getRawYCoord.sqrt != null, + "Schnorr nonce must be an x coordinate for which a quadratic residue y coordinate exists") + + def xCoord: FieldElement = { + FieldElement(bytes) + } +} + +object SchnorrNonce extends Factory[SchnorrNonce] { + + @tailrec + def fromBytes(bytes: ByteVector): SchnorrNonce = { + if (bytes.length == 32) { + new SchnorrNonce(bytes) + } else if (bytes.length < 32) { + // means we need to pad the private key with 0 bytes so we have 32 bytes + SchnorrNonce.fromBytes(bytes.padLeft(32)) + } else if (bytes.length == 33) { + // this is for the case when java serialies a BigInteger to 33 bytes to hold the signed num representation + SchnorrNonce.fromBytes(bytes.tail) + } else { + throw new IllegalArgumentException( + "Schnorr nonce cannot be greater than 33 bytes in size, got: " + + BitcoinSUtil.encodeHex(bytes) + " which is of size: " + bytes.size) + } + } + + def kFromBipSchnorr( + privKey: ECPrivateKey, + message: ByteVector, + auxRand: ByteVector): ECPrivateKey = { + val privKeyForUse = privKey.schnorrKey + + val randHash = CryptoUtil.sha256SchnorrAuxRand(auxRand).bytes + val maskedKey = randHash.xor(privKeyForUse.bytes) + + val nonceHash = CryptoUtil.sha256SchnorrNonce( + maskedKey ++ privKey.schnorrPublicKey.bytes ++ message) + + ECPrivateKey(nonceHash.bytes).nonceKey + } + + /** Computes the bip-schnorr nonce for a given message and private key. + * This is intended to ensure that no two messages are signed with the + * same nonce. + */ + def fromBipSchnorr( + privKey: ECPrivateKey, + message: ByteVector, + auxRand: ByteVector): SchnorrNonce = { + val k = kFromBipSchnorr(privKey, message, auxRand) + k.publicKey.schnorrNonce + } + + def apply(xCoor: FieldElement): SchnorrNonce = { + SchnorrNonce(xCoor.bytes) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/crypto/SchnorrPublicKey.scala b/core/src/main/scala/org/bitcoins/core/crypto/SchnorrPublicKey.scala new file mode 100644 index 0000000000..7728eb90d5 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/crypto/SchnorrPublicKey.scala @@ -0,0 +1,132 @@ +package org.bitcoins.core.crypto + +import org.bitcoin.{NativeSecp256k1, Secp256k1Context} +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.{BitcoinSUtil, Factory} +import scodec.bits.ByteVector + +import scala.annotation.tailrec +import scala.util.Try + +case class SchnorrPublicKey(bytes: ByteVector) extends NetworkElement { + require(bytes.length == 32, + s"Schnorr public keys must be 32 bytes, got $bytes") + require(Try(publicKey).isSuccess, + s"Schnorr public key must be a valid x coordinate, got $bytes") + + def verify(data: ByteVector, signature: SchnorrDigitalSignature): Boolean = { + verify(data, signature, Secp256k1Context.isEnabled) + } + + def verify( + data: ByteVector, + signature: SchnorrDigitalSignature, + useSecp: Boolean): Boolean = { + if (useSecp) { + verifyWithSecp(data, signature) + } else { + verifyWithBouncyCastle(data, signature) + } + } + + def verifyWithSecp( + data: ByteVector, + signature: SchnorrDigitalSignature): Boolean = { + NativeSecp256k1.schnorrVerify(signature.bytes.toArray, + data.toArray, + bytes.toArray) + } + + def verifyWithBouncyCastle( + data: ByteVector, + signature: SchnorrDigitalSignature): Boolean = { + BouncyCastleUtil.schnorrVerify(data, this, signature) + } + + def computeSigPoint(data: ByteVector, nonce: SchnorrNonce): ECPublicKey = { + computeSigPoint(data, nonce, compressed = true, Secp256k1Context.isEnabled) + } + + def computeSigPoint( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean): ECPublicKey = { + computeSigPoint(data, nonce, compressed, Secp256k1Context.isEnabled) + } + + def computeSigPoint( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean, + useSecp: Boolean): ECPublicKey = { + if (useSecp) { + computeSigPointWithSecp(data, nonce, compressed) + } else { + computeSigPointWithBouncyCastle(data, nonce, compressed) + } + } + + def computeSigPointWithSecp( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean = true): ECPublicKey = { + val sigPointBytes = NativeSecp256k1.schnorrComputeSigPoint( + data.toArray, + nonce.bytes.toArray, + bytes.toArray, + compressed) + ECPublicKey(ByteVector(sigPointBytes)) + } + + def computeSigPointWithBouncyCastle( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean = true): ECPublicKey = { + BouncyCastleUtil.schnorrComputeSigPoint(data, nonce, this, compressed) + } + + def publicKey: ECPublicKey = { + val pubKeyBytes = ByteVector.fromByte(2) ++ bytes + + val validPubKey = if (Secp256k1Context.isEnabled) { + NativeSecp256k1.isValidPubKey(pubKeyBytes.toArray) + } else { + BouncyCastleUtil.validatePublicKey(pubKeyBytes) + } + + require( + validPubKey, + s"Cannot construct schnorr public key from invalid x coordinate: $bytes") + + ECPublicKey(pubKeyBytes) + } + + def xCoord: FieldElement = FieldElement(bytes) +} + +object SchnorrPublicKey extends Factory[SchnorrPublicKey] { + + @tailrec + def fromBytes(bytes: ByteVector): SchnorrPublicKey = { + require(bytes.length <= 33, + s"XOnlyPublicKey must be less than 33 bytes, got $bytes") + + if (bytes.length == 32) + new SchnorrPublicKey(bytes) + else if (bytes.length < 32) { + // means we need to pad the private key with 0 bytes so we have 32 bytes + SchnorrPublicKey.fromBytes(bytes.padLeft(32)) + } else if (bytes.length == 33) { + // this is for the case when java serialies a BigInteger to 33 bytes to hold the signed num representation + SchnorrPublicKey.fromBytes(bytes.tail) + } else { + throw new IllegalArgumentException( + "XOnlyPublicKey cannot be greater than 33 bytes in size, got: " + + BitcoinSUtil.encodeHex(bytes) + " which is of size: " + bytes.size) + } + } + + def apply(xCoor: FieldElement): SchnorrPublicKey = { + SchnorrPublicKey(xCoor.bytes) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala b/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala index 170ebc8cfe..326f9388dd 100644 --- a/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/CryptoUtil.scala @@ -38,6 +38,51 @@ trait CryptoUtil extends BitcoinSLogger { sha256(bits.toByteVector) } + def taggedSha256(bytes: ByteVector, tag: String): Sha256Digest = { + val tagHash = sha256(ByteVector(tag.getBytes())) + val tagBytes = tagHash.bytes ++ tagHash.bytes + sha256(tagBytes ++ bytes) + } + + // The tag "BIP340/challenge" + private val schnorrChallengeTagBytes = { + ByteVector + .fromHex( + "07e00dcd3055c1b36ee93effe4d7f266024cdef4116982ff5dfdc1a97e77062907e00dcd3055c1b36ee93effe4d7f266024cdef4116982ff5dfdc1a97e770629" + ) + .get + } + + def sha256SchnorrChallenge(bytes: ByteVector): Sha256Digest = { + sha256(schnorrChallengeTagBytes ++ bytes) + } + + // The tag "BIP340/nonce" + private val schnorrNonceTagBytes = { + ByteVector + .fromHex( + "a2ba14a6b39c1c505260bf3aceb07febde3ab34c35c9259d25bd6972f15e6564a2ba14a6b39c1c505260bf3aceb07febde3ab34c35c9259d25bd6972f15e6564" + ) + .get + } + + def sha256SchnorrNonce(bytes: ByteVector): Sha256Digest = { + sha256(schnorrNonceTagBytes ++ bytes) + } + + // The tag "BIP340/aux" + private val schnorrAuxTagBytes = { + ByteVector + .fromHex( + "4b07426ad8630dcdbadf8dee1e94f09ac2df4e7ee2629e5e6b27c8666c8cf31e4b07426ad8630dcdbadf8dee1e94f09ac2df4e7ee2629e5e6b27c8666c8cf31e" + ) + .get + } + + def sha256SchnorrAuxRand(bytes: ByteVector): Sha256Digest = { + sha256(schnorrAuxTagBytes ++ bytes) + } + /** Performs SHA1(bytes). */ def sha1(bytes: ByteVector): Sha1Digest = { val hash = MessageDigest.getInstance("SHA-1").digest(bytes.toArray).toList diff --git a/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala b/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala index 605c8a8cbb..de742b9415 100644 --- a/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala @@ -2,6 +2,7 @@ package org.bitcoins.core.util import java.math.BigInteger +import org.bitcoins.core.crypto.FieldElement import org.bitcoins.core.number._ import org.bitcoins.core.protocol.blockchain.BlockHeader import org.bitcoins.core.protocol.blockchain.BlockHeader.TargetDifficultyHelper @@ -28,6 +29,10 @@ sealed abstract class NumberUtil extends BitcoinSLogger { toUnsignedInt(bytes.toArray) } + def uintToFieldElement(bytes: ByteVector): FieldElement = { + FieldElement(toUnsignedInt(bytes)) + } + /** Converts a sequence of bytes to a **big endian** unsigned integer */ def toUnsignedInt(bytes: Array[Byte]): BigInt = { BigInt(new BigInteger(1, bytes)) diff --git a/inThisBuild.sbt b/inThisBuild.sbt index 1f712b2a6c..d66b013049 100644 --- a/inThisBuild.sbt +++ b/inThisBuild.sbt @@ -1,3 +1,8 @@ +version in ThisBuild ~= { version => + val withoutSuffix = version.dropRight(8) + withoutSuffix + "SCHNORR-DLC-SNAPSHOT" +} + val scala2_11 = "2.11.12" val scala2_12 = "2.12.11" val scala2_13 = "2.13.1" diff --git a/secp256k1 b/secp256k1 index e0239255f1..c56f1d3246 160000 --- a/secp256k1 +++ b/secp256k1 @@ -1 +1 @@ -Subproject commit e0239255f1f386cf76279c008253b70cd17fe466 +Subproject commit c56f1d3246607edd0aa4bb3e1e8cf58b1e192c9b diff --git a/secp256k1jni/natives/linux_64/bench_ecdh b/secp256k1jni/natives/linux_64/bench_ecdh index ccc23a7c07..24ba0c36d4 100755 Binary files a/secp256k1jni/natives/linux_64/bench_ecdh and b/secp256k1jni/natives/linux_64/bench_ecdh differ diff --git a/secp256k1jni/natives/linux_64/bench_sign b/secp256k1jni/natives/linux_64/bench_sign index 41d94bc8ef..5f9bd8a428 100755 Binary files a/secp256k1jni/natives/linux_64/bench_sign and b/secp256k1jni/natives/linux_64/bench_sign differ diff --git a/secp256k1jni/natives/linux_64/bench_verify b/secp256k1jni/natives/linux_64/bench_verify index 56e20d2668..2ca1ed792a 100755 Binary files a/secp256k1jni/natives/linux_64/bench_verify and b/secp256k1jni/natives/linux_64/bench_verify differ diff --git a/secp256k1jni/natives/linux_64/libsecp256k1.a b/secp256k1jni/natives/linux_64/libsecp256k1.a index 280583698d..5580019f5c 100644 Binary files a/secp256k1jni/natives/linux_64/libsecp256k1.a and b/secp256k1jni/natives/linux_64/libsecp256k1.a differ diff --git a/secp256k1jni/natives/linux_64/libsecp256k1.so.0.0.0 b/secp256k1jni/natives/linux_64/libsecp256k1.so.0.0.0 index 15a6595b2b..4f9f947ca1 100755 Binary files a/secp256k1jni/natives/linux_64/libsecp256k1.so.0.0.0 and b/secp256k1jni/natives/linux_64/libsecp256k1.so.0.0.0 differ diff --git a/secp256k1jni/natives/osx_64/bench_ecdh b/secp256k1jni/natives/osx_64/bench_ecdh index 948e5e2042..65b99174c9 100755 Binary files a/secp256k1jni/natives/osx_64/bench_ecdh and b/secp256k1jni/natives/osx_64/bench_ecdh differ diff --git a/secp256k1jni/natives/osx_64/bench_sign b/secp256k1jni/natives/osx_64/bench_sign index 1f7a8a6c19..abcecdecc2 100755 Binary files a/secp256k1jni/natives/osx_64/bench_sign and b/secp256k1jni/natives/osx_64/bench_sign differ diff --git a/secp256k1jni/natives/osx_64/bench_verify b/secp256k1jni/natives/osx_64/bench_verify index aa79627fc8..471158c62b 100755 Binary files a/secp256k1jni/natives/osx_64/bench_verify and b/secp256k1jni/natives/osx_64/bench_verify differ diff --git a/secp256k1jni/natives/osx_64/libsecp256k1.0.dylib b/secp256k1jni/natives/osx_64/libsecp256k1.0.dylib index ad72410f10..8bf6bfbfa1 100755 Binary files a/secp256k1jni/natives/osx_64/libsecp256k1.0.dylib and b/secp256k1jni/natives/osx_64/libsecp256k1.0.dylib differ diff --git a/secp256k1jni/natives/osx_64/libsecp256k1.a b/secp256k1jni/natives/osx_64/libsecp256k1.a index 43bc9a8bf1..5d0d0afc20 100644 Binary files a/secp256k1jni/natives/osx_64/libsecp256k1.a and b/secp256k1jni/natives/osx_64/libsecp256k1.a differ diff --git a/secp256k1jni/natives/osx_64/libsecp256k1.dylib b/secp256k1jni/natives/osx_64/libsecp256k1.dylib index ad72410f10..8bf6bfbfa1 100755 Binary files a/secp256k1jni/natives/osx_64/libsecp256k1.dylib and b/secp256k1jni/natives/osx_64/libsecp256k1.dylib differ diff --git a/secp256k1jni/natives/osx_64/libsecp256k1_jni.a b/secp256k1jni/natives/osx_64/libsecp256k1_jni.a index 1bae720698..59319f1558 100644 Binary files a/secp256k1jni/natives/osx_64/libsecp256k1_jni.a and b/secp256k1jni/natives/osx_64/libsecp256k1_jni.a differ diff --git a/secp256k1jni/natives/windows_64/libsecp256k1-0.dll b/secp256k1jni/natives/windows_64/libsecp256k1-0.dll index 9e0a57f5e9..0d2c26520e 100755 Binary files a/secp256k1jni/natives/windows_64/libsecp256k1-0.dll and b/secp256k1jni/natives/windows_64/libsecp256k1-0.dll differ diff --git a/secp256k1jni/src/main/java/org/bitcoin/NativeSecp256k1.java b/secp256k1jni/src/main/java/org/bitcoin/NativeSecp256k1.java index 51333db386..fffd4ad44c 100644 --- a/secp256k1jni/src/main/java/org/bitcoin/NativeSecp256k1.java +++ b/secp256k1jni/src/main/java/org/bitcoin/NativeSecp256k1.java @@ -457,6 +457,140 @@ public class NativeSecp256k1 { return resArr; } + /** + * libsecp256k1 schnorr sign - generates a BIP 340 Schnorr signature + * + * @param data message to sign + * @param secKey key to sign with + */ + public static byte[] schnorrSign(byte[] data, byte[] secKey, byte[] auxRand) throws AssertFailException { + checkArgument(data.length == 32 && secKey.length == 32 && auxRand.length == 32); + + ByteBuffer byteBuff = nativeECDSABuffer.get(); + if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) { + byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32); + byteBuff.order(ByteOrder.nativeOrder()); + nativeECDSABuffer.set(byteBuff); + } + byteBuff.rewind(); + byteBuff.put(data); + byteBuff.put(secKey); + byteBuff.put(auxRand); + + byte[][] retByteArray; + r.lock(); + try { + retByteArray = secp256k1_schnorrsig_sign(byteBuff, Secp256k1Context.getContext()); + } finally { + r.unlock(); + } + + byte[] sigArray = retByteArray[0]; + int retVal = new BigInteger(new byte[] { retByteArray[1][0] }).intValue(); + + assertEquals(retVal, 1, "Failed return value check."); + + return sigArray; + } + + /** + * libsecp256k1 schnorr sign - generates a BIP 340 Schnorr signature + * + * @param data message to sign + * @param secKey key to sign with + * @param nonce the nonce (k value) used in signing + */ + public static byte[] schnorrSignWithNonce(byte[] data, byte[] secKey, byte[] nonce) throws AssertFailException { + checkArgument(data.length == 32 && secKey.length == 32 && nonce.length == 32); + + ByteBuffer byteBuff = nativeECDSABuffer.get(); + if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) { + byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32); + byteBuff.order(ByteOrder.nativeOrder()); + nativeECDSABuffer.set(byteBuff); + } + byteBuff.rewind(); + byteBuff.put(data); + byteBuff.put(secKey); + byteBuff.put(nonce); + + byte[][] retByteArray; + r.lock(); + try { + retByteArray = secp256k1_schnorrsig_sign_with_nonce(byteBuff, Secp256k1Context.getContext()); + } finally { + r.unlock(); + } + + byte[] sigArray = retByteArray[0]; + int retVal = new BigInteger(new byte[]{retByteArray[1][0]}).intValue(); + + assertEquals(retVal, 1, "Failed return value check."); + + return sigArray; + } + + public static byte[] schnorrComputeSigPoint(byte[] data, byte[] nonce, byte[] pubkey, boolean compressed) throws AssertFailException { + checkArgument(data.length == 32 && nonce.length == 32 && pubkey.length == 32); + + ByteBuffer byteBuff = nativeECDSABuffer.get(); + if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) { + byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32); + byteBuff.order(ByteOrder.nativeOrder()); + nativeECDSABuffer.set(byteBuff); + } + byteBuff.rewind(); + byteBuff.put(data); + byteBuff.put(nonce); + byteBuff.put(pubkey); + + byte[][] retByteArray; + r.lock(); + try { + retByteArray = secp256k1_schnorrsig_compute_sigpoint(byteBuff, Secp256k1Context.getContext(), compressed); + } finally { + r.unlock(); + } + + byte[] pointArray = retByteArray[0]; + int outputLen = new BigInteger(new byte[] { retByteArray[1][0] }).intValue() & 0xFF; + int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue(); + + assertEquals(pointArray.length, outputLen, "Got bad point length."); + assertEquals(retVal, 1, "Failed return value check."); + + return pointArray; + } + + /** + * libsecp256k1 schnorr verify - verifies BIP 340 Schnorr signatures + * + * @param sig signature to verify + * @param data message the signature has signed + * @param pubx the key that did the signing + */ + public static boolean schnorrVerify(byte[] sig, byte[] data, byte[] pubx) throws AssertFailException { + checkArgument(sig.length == 64 && data.length == 32 && pubx.length == 32); + + ByteBuffer byteBuffer = nativeECDSABuffer.get(); + if (byteBuffer == null || byteBuffer.capacity() < 64 + 32 + 32) { + byteBuffer = ByteBuffer.allocateDirect(64 + 32 + 32); + byteBuffer.order(ByteOrder.nativeOrder()); + nativeECDSABuffer.set(byteBuffer); + } + byteBuffer.rewind(); + byteBuffer.put(sig); + byteBuffer.put(data); + byteBuffer.put(pubx); + + r.lock(); + try { + return secp256k1_schnorrsig_verify(byteBuffer, Secp256k1Context.getContext()) == 1; + } finally { + r.unlock(); + } + } + /** * libsecp256k1 randomize - updates the context randomization * @@ -508,4 +642,11 @@ public class NativeSecp256k1 { private static native byte[][] secp256k1_ecdh(ByteBuffer byteBuff, long context, int inputLen); + private static native byte[][] secp256k1_schnorrsig_sign(ByteBuffer byteBuff, long context); + + private static native byte[][] secp256k1_schnorrsig_sign_with_nonce(ByteBuffer byteBuff, long context); + + private static native byte[][] secp256k1_schnorrsig_compute_sigpoint(ByteBuffer byteBuff, long context, boolean compressed); + + private static native int secp256k1_schnorrsig_verify(ByteBuffer byteBuffer, long context); } diff --git a/secp256k1jni/src/test/java/org/bitcoin/NativeSecp256k1Test.java b/secp256k1jni/src/test/java/org/bitcoin/NativeSecp256k1Test.java index 20c305cf0a..7d59ea3e2c 100644 --- a/secp256k1jni/src/test/java/org/bitcoin/NativeSecp256k1Test.java +++ b/secp256k1jni/src/test/java/org/bitcoin/NativeSecp256k1Test.java @@ -229,6 +229,52 @@ public class NativeSecp256k1Test { assertEquals( ecdhString, "2A2A67007A926E6594AF3EB564FC74005B37A9C8AEF2033C4552051B5C87F043" , "testCreateECDHSecret"); } + @Test + public void testSchnorrSign() throws AssertFailException{ + byte[] data = toByteArray("E48441762FB75010B2AA31A512B62B4148AA3FB08EB0765D76B252559064A614"); + byte[] secKey = toByteArray("688C77BC2D5AAFF5491CF309D4753B732135470D05B7B2CD21ADD0744FE97BEF"); + byte[] auxRand = toByteArray("02CCE08E913F22A36C5648D6405A2C7C50106E7AA2F1649E381C7F09D16B80AB"); + + byte[] sigArr = NativeSecp256k1.schnorrSign(data, secKey, auxRand); + String sigStr = toHex(sigArr); + String expectedSig = "F14D7E54FF58C5D019CE9986BE4A0E8B7D643BD08EF2CDF1099E1A457865B5477C988C51634A8DC955950A58FF5DC8C506DDB796121E6675946312680C26CF33"; + assertEquals(sigStr, expectedSig, "testSchnorrSign"); + } + + @Test + public void testSchnorrSignWithNonce() throws AssertFailException{ + byte[] data = toByteArray("E48441762FB75010B2AA31A512B62B4148AA3FB08EB0765D76B252559064A614"); + byte[] secKey = toByteArray("688C77BC2D5AAFF5491CF309D4753B732135470D05B7B2CD21ADD0744FE97BEF"); + byte[] nonce = toByteArray("8C8CA771D3C25EB38DE7401818EEDA281AC5446F5C1396148F8D9D67592440FE"); + + byte[] sigArr = NativeSecp256k1.schnorrSignWithNonce(data, secKey, nonce); + String sigStr = toHex(sigArr); + String expectedSig = "5DA618C1936EC728E5CCFF29207F1680DCF4146370BDCFAB0039951B91E3637A50A2A860B130D009405511C3EAFE943E157A0DF2C2020E3E50DF05ADB175332F"; + assertEquals(sigStr, expectedSig, "testSchnorrSignWithNonce"); + } + + @Test + public void testSchnorrComputeSigPoint() throws AssertFailException{ + byte[] data = toByteArray("E48441762FB75010B2AA31A512B62B4148AA3FB08EB0765D76B252559064A614"); + byte[] nonce = toByteArray("F14D7E54FF58C5D019CE9986BE4A0E8B7D643BD08EF2CDF1099E1A457865B547"); + byte[] pubKey = toByteArray("B33CC9EDC096D0A83416964BD3C6247B8FECD256E4EFA7870D2C854BDEB33390"); + + byte[] pointArr = NativeSecp256k1.schnorrComputeSigPoint(data, nonce, pubKey, true); + String pointStr = toHex(pointArr); + String expectedPoint = "020D17280B8D2C2BD3B597B4446419C151DC237353D0FB9EC03D4EB7E8DE7EE0A8"; + assertEquals(pointStr, expectedPoint, "testSchnorrComputeSigPoint"); + } + + @Test + public void testSchnorrVerify() throws AssertFailException{ + byte[] sig = toByteArray("F14D7E54FF58C5D019CE9986BE4A0E8B7D643BD08EF2CDF1099E1A457865B5477C988C51634A8DC955950A58FF5DC8C506DDB796121E6675946312680C26CF33"); + byte[] data = toByteArray("E48441762FB75010B2AA31A512B62B4148AA3FB08EB0765D76B252559064A614"); + byte[] pubx = toByteArray("B33CC9EDC096D0A83416964BD3C6247B8FECD256E4EFA7870D2C854BDEB33390"); + + boolean result = NativeSecp256k1.schnorrVerify(sig, data, pubx); + + assertEquals(result, true, "testSchnorrVerify"); + } //https://stackoverflow.com/a/19119453/967713 private static byte[] toByteArray(final String hex) { diff --git a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala index 4b9fc4346d..4a52c9738c 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala @@ -118,6 +118,36 @@ sealed abstract class CryptoGenerators { def privateKey: Gen[ECPrivateKey] = Gen.delay(ECPrivateKey()) + def fieldElement: Gen[FieldElement] = privateKey.map(_.fieldElement) + + def smallFieldElement: Gen[FieldElement] = + NumberGenerator + .bytevector(30) + .map(bytes => FieldElement(ByteVector.fill(2)(0) ++ bytes)) + + def reallySmallFieldElement: Gen[FieldElement] = + NumberGenerator + .bytevector(15) + .map(bytes => FieldElement(ByteVector.fill(17)(0) ++ bytes)) + + def largeFieldElement: Gen[FieldElement] = + NumberGenerator + .bytevector(30) + .map(bytes => FieldElement(ByteVector.fill(2)(Byte.MinValue) ++ bytes)) + + def nonZeroFieldElement: Gen[FieldElement] = + nonZeroPrivKey.map(_.fieldElement) + + /** Generates a random non-zero private key */ + def nonZeroPrivKey: Gen[ECPrivateKey] = + privateKey.filter(_.bytes.toArray.exists(_ != 0.toByte)) + + def schnorrNonce: Gen[SchnorrNonce] = + nonZeroPrivKey.map(_.publicKey.bytes.tail).map(SchnorrNonce.fromBytes) + + def schnorrPublicKey: Gen[SchnorrPublicKey] = + publicKey.map(_.schnorrPublicKey) + /** * Generate a sequence of private keys * @param num maximum number of keys to generate @@ -174,6 +204,13 @@ sealed abstract class CryptoGenerators { hash <- CryptoGenerators.doubleSha256Digest } yield privKey.sign(hash) + def schnorrDigitalSignature: Gen[SchnorrDigitalSignature] = { + for { + privKey <- privateKey + hash <- CryptoGenerators.doubleSha256Digest + } yield privKey.schnorrSign(hash.bytes) + } + def sha256Digest: Gen[Sha256Digest] = for { bytes <- NumberGenerator.bytevector