Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] k256: Taproot Schnorr (BIP 340) #482

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/k256.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ jobs:
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features keccak256
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pem
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pkcs8
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features schnorr
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features serde
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features sha256
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features ecdsa,keccak256
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features ecdsa,sha256
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features arithmetic,bits,ecdh,ecdsa,jwk,keccak256,pem,pkcs8,serde,sha256
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features arithmetic,bits,ecdh,ecdsa,jwk,keccak256,pem,pkcs8,schnorr,serde,sha256

benches:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions k256/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ ecdsa-core = { version = "0.13", package = "ecdsa", optional = true, default-fea
hex-literal = { version = "0.3", optional = true }
sha2 = { version = "0.9", optional = true, default-features = false }
sha3 = { version = "0.9", optional = true, default-features = false }
signature = { version = ">= 1.3.1, <1.5", optional = true, default-features = false }

[dev-dependencies]
blobby = "0.3"
Expand All @@ -50,6 +51,7 @@ jwk = ["elliptic-curve/jwk"]
keccak256 = ["digest", "sha3"]
pem = ["elliptic-curve/pem", "ecdsa-core/pem", "pkcs8"]
pkcs8 = ["elliptic-curve/pkcs8"]
schnorr = ["arithmetic", "sha256", "signature/digest-preview"]
serde = ["ecdsa-core/serde", "elliptic-curve/serde", "sec1/serde"]
sha256 = ["digest", "sha2"]
std = ["ecdsa-core/std", "elliptic-curve/std"] # TODO: use weak activation for `ecdsa-core/std` when available
Expand Down
44 changes: 28 additions & 16 deletions k256/src/arithmetic/affine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use elliptic_curve::{
sec1::{self, FromEncodedPoint, ToEncodedPoint},
subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption},
zeroize::DefaultIsZeroes,
AffineArithmetic, AffineXCoordinate, DecompressPoint, Error, Result,
AffineArithmetic, AffineXCoordinate, DecompactPoint, DecompressPoint, Error, Result,
};

impl AffineArithmetic for Secp256k1 {
Expand Down Expand Up @@ -41,6 +41,17 @@ pub struct AffinePoint {
pub(super) infinity: Choice,
}

impl AffinePoint {
/// Create a new [`AffinePoint`] with the given coordinates.
pub(crate) fn new(x: FieldElement, y: FieldElement) -> Self {
Self {
x,
y,
infinity: Choice::from(0),
}
}
}

impl PrimeCurveAffine for AffinePoint {
type Scalar = Scalar;
type Curve = ProjectivePoint;
Expand All @@ -59,21 +70,20 @@ impl PrimeCurveAffine for AffinePoint {
// SECP256k1 basepoint in affine coordinates:
// x = 79be667e f9dcbbac 55a06295 ce870b07 029bfcdb 2dce28d9 59f2815b 16f81798
// y = 483ada77 26a3c465 5da4fbfc 0e1108a8 fd17b448 a6855419 9c47d08f fb10d4b8
AffinePoint {
x: FieldElement::from_bytes(&arr![u8;
Self::new(
FieldElement::from_bytes(&arr![u8;
0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87,
0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b,
0x16, 0xf8, 0x17, 0x98
])
.unwrap(),
y: FieldElement::from_bytes(&arr![u8;
FieldElement::from_bytes(&arr![u8;
0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc, 0x0e, 0x11,
0x08, 0xa8, 0xfd, 0x17, 0xb4, 0x48, 0xa6, 0x85, 0x54, 0x19, 0x9c, 0x47, 0xd0, 0x8f,
0xfb, 0x10, 0xd4, 0xb8
])
.unwrap(),
infinity: Choice::from(0),
}
)
}

/// Is this point the identity point?
Expand Down Expand Up @@ -140,16 +150,22 @@ impl DecompressPoint<Secp256k1> for AffinePoint {
beta.normalize().is_odd().ct_eq(&y_is_odd),
);

Self {
x,
y: y.normalize(),
infinity: Choice::from(0),
}
Self::new(x, y.normalize())
})
})
}
}

/// Decompaction using Taproot conventions as described in [BIP 340].
///
/// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
impl DecompactPoint<Secp256k1> for AffinePoint {
fn decompact(x_bytes: &FieldBytes) -> CtOption<Self> {
// TODO(tarcieri): implement full `lift_x` algorithm as described in BIP 340
Copy link
Member Author

@tarcieri tarcieri Dec 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BIP 340 defines the lift_x function as follows:

The function lift_x(x), where x is an integer in range 0..p-1, returns the point P for which x(P) = x

Given a candidate X coordinate ''x'' in the range ''0..p-1'', there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then ''x'' is not a valid X coordinate either, i.e., no point ''P'' exists for which ''x(P) = x''.

The valid Y coordinates for a given candidate ''x'' are the square roots of ''c = x3 + 7 mod p'' and they can be computed as ''y = ±c(p+1)/4 mod p'' (see 1) if they exist, which can be checked by squaring and comparing with ''c'' and ''has_even_y(P)'', or fails if no such point exists. The function ''lift_x(x)'' is equivalent to the following pseudocode:

  • Let c = x3 + 7 mod p.
  • Let y = c(p+1)/4 mod p.
  • Fail if c ≠ y2 mod p.
  • Return the unique point P such that x(P) = x and y(P) = y if y mod 2 = 0 or y(P) = p-y otherwise.

Unfortunately I'm unable to compute y = c(p+1)/4 mod p as the only support we have for raising a FieldElement to a power is FieldElement::pow2k which requires the exponent be a power of 2.

cc @fjarri

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I missing something? To me it seems like FieldElement::sqrt exactly implements this power of (p+1)/4. It's a bit less unrolled than https://github.com/decred/dcrd/blob/46305c7bf5e0f36f88a2d5e0cea89b4480e9e69a/dcrec/secp256k1/field.go#L1108 (which is what taurusgroup/multi-party-sig pulls in), but otherwise equivalent - I think.

Self::decompress(x_bytes, Choice::from(0))
}
}

impl GroupEncoding for AffinePoint {
type Repr = CompressedPoint;

Expand Down Expand Up @@ -203,11 +219,7 @@ impl FromEncodedPoint<Secp256k1> for AffinePoint {
// Check that the point is on the curve
let lhs = (y * &y).negate(1);
let rhs = x * &x * &x + &CURVE_EQUATION_B;
let point = AffinePoint {
x,
y,
infinity: Choice::from(0),
};
let point = Self::new(x, y);
CtOption::new(point, (lhs + &rhs).normalizes_to_zero())
})
})
Expand Down
13 changes: 11 additions & 2 deletions k256/src/arithmetic/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl FieldElement {
Self(FieldElementImpl::one())
}

/// Determine if this `FieldElement10x26` is zero.
/// Determine if this `FieldElement` is zero.
///
/// # Returns
///
Expand All @@ -63,7 +63,16 @@ impl FieldElement {
self.0.is_zero()
}

/// Determine if this `FieldElement10x26` is odd in the SEC1 sense: `self mod 2 == 1`.
/// Determine if this `FieldElement` is even in the SEC1 sense: `self mod 2 == 0`.
///
/// # Returns
///
/// If even, return `Choice(1)`. Otherwise, return `Choice(0)`.
pub fn is_even(&self) -> Choice {
!self.0.is_odd()
}

/// Determine if this `FieldElement` is odd in the SEC1 sense: `self mod 2 == 1`.
///
/// # Returns
///
Expand Down
6 changes: 1 addition & 5 deletions k256/src/arithmetic/projective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,7 @@ impl ProjectivePoint {
pub fn to_affine(&self) -> AffinePoint {
self.z
.invert()
.map(|zinv| AffinePoint {
x: self.x * &zinv,
y: self.y * &zinv,
infinity: Choice::from(0),
})
.map(|zinv| AffinePoint::new(self.x * &zinv, self.y * &zinv))
.unwrap_or_else(AffinePoint::identity)
}

Expand Down
4 changes: 4 additions & 0 deletions k256/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub mod ecdh;
#[cfg_attr(docsrs, doc(cfg(feature = "ecdsa-core")))]
pub mod ecdsa;

#[cfg(feature = "schnorr")]
#[cfg_attr(docsrs, doc(cfg(feature = "schnorr")))]
pub mod schnorr;

#[cfg(any(feature = "test-vectors", test))]
#[cfg_attr(docsrs, doc(cfg(feature = "test-vectors")))]
pub mod test_vectors;
Expand Down
Loading