mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-26 21:42:48 +01:00
Added data structure for x-only public keys with undetermined parity (#4387)
This commit is contained in:
parent
762202a54d
commit
7e2ecd9d6a
7 changed files with 177 additions and 0 deletions
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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] {
|
||||
|
|
31
crypto/src/main/scala/org/bitcoins/crypto/KeyParity.scala
Normal file
31
crypto/src/main/scala/org/bitcoins/crypto/KeyParity.scala
Normal 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)
|
||||
}
|
|
@ -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] {
|
||||
|
|
53
crypto/src/main/scala/org/bitcoins/crypto/XOnlyPubKey.scala
Normal file
53
crypto/src/main/scala/org/bitcoins/crypto/XOnlyPubKey.scala
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue