Added data structure for x-only public keys with undetermined parity (#4387)

This commit is contained in:
Nadav Kohen 2022-06-13 16:02:37 -05:00 committed by GitHub
parent 762202a54d
commit 7e2ecd9d6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 177 additions and 0 deletions

View file

@ -40,6 +40,8 @@ sealed abstract class CryptoGenerators {
def schnorrPublicKey: Gen[SchnorrPublicKey] =
publicKey.map(_.schnorrPublicKey)
def xOnlyPubKey: Gen[XOnlyPubKey] = publicKey.map(_.toXOnly)
/** Generate a sequence of private keys
* @param num maximum number of keys to generate
* @return

View file

@ -0,0 +1,27 @@
package org.bitcoins.crypto
import scodec.bits.ByteVector
class KeyParityTest extends BitcoinSCryptoTest {
behavior of "KeyParity"
it must "use 0x02 for even and 0x03 for odd" in {
assert(EvenParity.bytes == ByteVector.fromByte(0x02))
assert(OddParity.bytes == ByteVector.fromByte(0x03))
assert(KeyParity.fromByte(0x02) == EvenParity)
assert(KeyParity.fromByte(0x03) == OddParity)
assert(KeyParity(ByteVector.fromByte(0x02)) == EvenParity)
assert(KeyParity(ByteVector.fromByte(0x03)) == OddParity)
}
it must "fail to construct parity from bytes other than 0x02 or 0x03" in {
forAll(NumberGenerator.byte.filterNot(b => b == 0x02 || b == 0x03)) {
byte =>
assertThrows[IllegalArgumentException](KeyParity.fromByte(byte))
}
forAll(NumberGenerator.bytevector.filter(_.length != 1)) { bytes =>
assertThrows[IllegalArgumentException](KeyParity(bytes))
}
}
}

View file

@ -0,0 +1,58 @@
package org.bitcoins.crypto
class XOnlyPubKeyTest extends BitcoinSCryptoTest {
behavior of "XOnlyPubKey"
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
it must "fail for incorrect lengths" in {
assertThrows[IllegalArgumentException](
XOnlyPubKey(
"676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a"))
assertThrows[IllegalArgumentException](
XOnlyPubKey(
"676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a010203"))
}
it must "fail for invalid x coordinate" in {
assertThrows[IllegalArgumentException](
XOnlyPubKey(
"EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34"))
assertThrows[IllegalArgumentException](
XOnlyPubKey(
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"))
}
it must "succeed for valid large x coordinates above the curve order" in {
val _ = XOnlyPubKey(
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2c").coord
succeed
}
it must "have serialization symmetry" in {
forAll(CryptoGenerators.xOnlyPubKey) { xOnlyPub =>
assert(XOnlyPubKey(xOnlyPub.bytes) == xOnlyPub)
assert(XOnlyPubKey(xOnlyPub.coord) == xOnlyPub)
}
}
it must "correctly go back and forth between x-only and ECPublicKey" in {
forAll(CryptoGenerators.publicKey) { pubKey =>
val parity = pubKey.parity
val xOnly = pubKey.toXOnly
assert(xOnly.publicKey(parity) == pubKey)
}
}
it must "correctly go back and forth between x-only and SchnorrPubKey" in {
forAll(CryptoGenerators.schnorrPublicKey) { pubKey =>
val xOnly = pubKey.toXOnly
assert(xOnly.schnorrPublicKey == pubKey)
}
}
}

View file

@ -392,6 +392,10 @@ case class ECPublicKey(private val _bytes: ByteVector)
def tweakMultiply(tweak: FieldElement): ECPublicKey = {
CryptoUtil.tweakMultiply(this, tweak)
}
def toXOnly: XOnlyPubKey = XOnlyPubKey(bytes.drop(1))
def parity: KeyParity = KeyParity.fromByte(bytes.head)
}
object ECPublicKey extends Factory[ECPublicKey] {

View file

@ -0,0 +1,31 @@
package org.bitcoins.crypto
import scodec.bits.ByteVector
sealed trait KeyParity extends NetworkElement
object KeyParity extends Factory[KeyParity] {
override def fromBytes(bytes: ByteVector): KeyParity = {
require(bytes.length == 1, s"Parity must be a single byte, got $bytes")
bytes.head match {
case 0x02 => EvenParity
case 0x03 => OddParity
case b =>
throw new IllegalArgumentException(s"Unexpected parity byte: $b")
}
}
def fromByte(byte: Byte): KeyParity = {
fromBytes(ByteVector.fromByte(byte))
}
}
case object EvenParity extends KeyParity {
override val bytes: ByteVector = ByteVector.fromByte(0x02)
}
case object OddParity extends KeyParity {
override val bytes: ByteVector = ByteVector.fromByte(0x03)
}

View file

@ -63,6 +63,8 @@ case class SchnorrPublicKey(bytes: ByteVector) extends NetworkElement {
}
def xCoord: CurveCoordinate = CurveCoordinate(bytes)
def toXOnly: XOnlyPubKey = XOnlyPubKey(bytes)
}
object SchnorrPublicKey extends Factory[SchnorrPublicKey] {

View file

@ -0,0 +1,53 @@
package org.bitcoins.crypto
import scodec.bits.ByteVector
import scala.annotation.tailrec
import scala.util.Try
/** Represents the x-coordinate of an ECPublicKey, with undetermined y-coordinate parity */
case class XOnlyPubKey(bytes: ByteVector) extends NetworkElement {
require(bytes.length == 32,
s"x-only public keys must be 32 bytes, got $bytes")
require(Try(publicKey(EvenParity)).isSuccess,
s"x-only public key must be a valid x coordinate, got $bytes")
def publicKey(parity: KeyParity): ECPublicKey = {
val pubKeyBytes = parity.bytes ++ bytes
ECPublicKey(pubKeyBytes)
}
def schnorrPublicKey: SchnorrPublicKey = {
SchnorrPublicKey(bytes)
}
def coord: CurveCoordinate = CurveCoordinate(bytes)
}
object XOnlyPubKey extends Factory[XOnlyPubKey] {
@tailrec
def fromBytes(bytes: ByteVector): XOnlyPubKey = {
require(bytes.length <= 33,
s"XOnlyPublicKey must be less than 33 bytes, got $bytes")
if (bytes.length == 32)
new XOnlyPubKey(bytes)
else if (bytes.length < 32) {
// means we need to pad the private key with 0 bytes so we have 32 bytes
XOnlyPubKey.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
XOnlyPubKey.fromBytes(bytes.tail)
} else {
throw new IllegalArgumentException(
"XOnlyPublicKey cannot be greater than 33 bytes in size, got: " +
CryptoBytesUtil.encodeHex(bytes) + " which is of size: " + bytes.size)
}
}
def apply(coord: CurveCoordinate): XOnlyPubKey = {
XOnlyPubKey(coord.bytes)
}
}