mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +00:00
Expand tests for ethereum-schnorr-contract
This commit is contained in:
@@ -31,8 +31,7 @@ impl PublicKey {
|
||||
|
||||
let x_coordinate = affine.x();
|
||||
// Return None if the x-coordinate isn't mutual to both fields
|
||||
// While reductions shouldn't be an issue, it's one less headache/concern to have
|
||||
// The trivial amount of public keys this makes non-representable aren't a concern
|
||||
// The trivial amount of public keys this makes non-representable aren't considered a concern
|
||||
if <Scalar as Reduce<KU256>>::reduce_bytes(&x_coordinate).to_repr() != x_coordinate {
|
||||
None?;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,17 @@ pub struct Signature {
|
||||
impl Signature {
|
||||
/// Construct a new `Signature`.
|
||||
#[must_use]
|
||||
pub fn new(c: Scalar, s: Scalar) -> Signature {
|
||||
Signature { c, s }
|
||||
pub fn new(c: Scalar, s: Scalar) -> Option<Signature> {
|
||||
if bool::from(c.is_zero()) {
|
||||
None?;
|
||||
}
|
||||
Some(Signature { c, s })
|
||||
}
|
||||
|
||||
/// The challenge for a signature.
|
||||
///
|
||||
/// With negligible probability, this MAY return 0 which will create an invalid/unverifiable
|
||||
/// signature.
|
||||
#[must_use]
|
||||
pub fn challenge(R: ProjectivePoint, key: &PublicKey, message: &[u8]) -> Scalar {
|
||||
// H(R || A || m)
|
||||
|
||||
@@ -17,6 +17,9 @@ use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||
|
||||
use crate::{PublicKey, Signature};
|
||||
|
||||
mod public_key;
|
||||
pub(crate) use public_key::test_key;
|
||||
mod signature;
|
||||
mod premise;
|
||||
|
||||
#[expect(warnings)]
|
||||
@@ -88,12 +91,7 @@ async fn test_verify() {
|
||||
let (_anvil, provider, address) = setup_test().await;
|
||||
|
||||
for _ in 0 .. 100 {
|
||||
let (key, public_key) = loop {
|
||||
let key = Scalar::random(&mut OsRng);
|
||||
if let Some(public_key) = PublicKey::new(ProjectivePoint::GENERATOR * key) {
|
||||
break (key, public_key);
|
||||
}
|
||||
};
|
||||
let (key, public_key) = test_key();
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let mut message = vec![0; 1 + usize::try_from(OsRng.next_u32() % 256).unwrap()];
|
||||
@@ -102,11 +100,37 @@ async fn test_verify() {
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &public_key, &message);
|
||||
let s = nonce + (c * key);
|
||||
|
||||
let sig = Signature::new(c, s);
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
assert!(sig.verify(&public_key, &message));
|
||||
assert!(call_verify(&provider, address, &public_key, &message, &sig).await);
|
||||
|
||||
// Test setting `s = 0` doesn't pass verification
|
||||
{
|
||||
let zero_s = Signature::new(c, Scalar::ZERO).unwrap();
|
||||
assert!(!zero_s.verify(&public_key, &message));
|
||||
assert!(!call_verify(&provider, address, &public_key, &message, &zero_s).await);
|
||||
}
|
||||
|
||||
// Mutate the message and make sure the signature now fails to verify
|
||||
message[0] = message[0].wrapping_add(1);
|
||||
assert!(!call_verify(&provider, address, &public_key, &message, &sig).await);
|
||||
{
|
||||
let mut message = message.clone();
|
||||
message[0] = message[0].wrapping_add(1);
|
||||
assert!(!sig.verify(&public_key, &message));
|
||||
assert!(!call_verify(&provider, address, &public_key, &message, &sig).await);
|
||||
}
|
||||
|
||||
// Mutate c and make sure the signature now fails to verify
|
||||
{
|
||||
let mutated_c = Signature::new(c + Scalar::ONE, s).unwrap();
|
||||
assert!(!mutated_c.verify(&public_key, &message));
|
||||
assert!(!call_verify(&provider, address, &public_key, &message, &mutated_c).await);
|
||||
}
|
||||
|
||||
// Mutate s and make sure the signature now fails to verify
|
||||
{
|
||||
let mutated_s = Signature::new(c, s + Scalar::ONE).unwrap();
|
||||
assert!(!mutated_s.verify(&public_key, &message));
|
||||
assert!(!call_verify(&provider, address, &public_key, &message, &mutated_s).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use k256::{
|
||||
|
||||
use alloy_core::primitives::Address;
|
||||
|
||||
use crate::{PublicKey, Signature};
|
||||
use crate::{Signature, tests::test_key};
|
||||
|
||||
// The ecrecover opcode, yet with if the y is odd replacing v
|
||||
fn ecrecover(message: Scalar, odd_y: bool, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
||||
@@ -64,12 +64,7 @@ fn test_ecrecover() {
|
||||
// of efficiently verifying Schnorr signatures in an Ethereum contract
|
||||
#[test]
|
||||
fn nonce_recovery_via_ecrecover() {
|
||||
let (key, public_key) = loop {
|
||||
let key = Scalar::random(&mut OsRng);
|
||||
if let Some(public_key) = PublicKey::new(ProjectivePoint::GENERATOR * key) {
|
||||
break (key, public_key);
|
||||
}
|
||||
};
|
||||
let (key, public_key) = test_key();
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let R = ProjectivePoint::GENERATOR * nonce;
|
||||
@@ -81,26 +76,28 @@ fn nonce_recovery_via_ecrecover() {
|
||||
let s = nonce + (c * key);
|
||||
|
||||
/*
|
||||
An ECDSA signature is `(r, s)` with `s = (H(m) + rx) / k`, where:
|
||||
- `m` is the message
|
||||
An ECDSA signature is `(r, s)` with `s = (m + (r * x)) / k`, where:
|
||||
- `m` is the hash of the message
|
||||
- `r` is the x-coordinate of the nonce, reduced into a scalar
|
||||
- `x` is the private key
|
||||
- `k` is the nonce
|
||||
|
||||
We fix the recovery ID to be for the even key with an x-coordinate < the order. Accordingly,
|
||||
`kG = Point::from(Even, r)`. This enables recovering the public key via
|
||||
`((s Point::from(Even, r)) - H(m)G) / r`.
|
||||
`k * G = Point::from(Even, r)`. This enables recovering the public key via
|
||||
`((s * Point::from(Even, r)) - (m * G)) / r`.
|
||||
|
||||
We want to calculate `R` from `(c, s)` where `s = r + cx`. That means we need to calculate
|
||||
`sG - cX`.
|
||||
`(s * G) - (c * X)`.
|
||||
|
||||
We can calculate `sG - cX` with `((s Point::from(Even, r)) - H(m)G) / r` if:
|
||||
- Latter `r` = `X.x`
|
||||
- Latter `s` = `c`
|
||||
- `H(m)` = former `s`
|
||||
This gets us to `(cX - sG) / X.x`. If we additionally scale the latter's `s, H(m)` values (the
|
||||
former's `c, s` values) by `X.x`, we get `cX - sG`. This just requires negating each to achieve
|
||||
`sG - cX`.
|
||||
We can calculate `(s * G) - (c * X)` with `((s * Point::from(Even, r)) - (m * G)) / r` if:
|
||||
- ECDSA `r` = `X.x`, the x-coordinate of the Schnorr public key
|
||||
- ECDSA `s` = `c`, the Schnorr signature's challenge
|
||||
- ECDSA `m` = Schnorr `s`
|
||||
This gets us to `((c * X) - (s * G)) / X.x`. If we additionally scale the ECDSA `s, m` values
|
||||
(the Schnorr `c, s` values) by `X.x`, we get `(c * X) - (s * G)`. This just requires negating
|
||||
to achieve `(s * G) - (c * X)`.
|
||||
|
||||
With `R`, we can recalculate and compare the challenges to confirm the signature is valid.
|
||||
*/
|
||||
let x_scalar = <Scalar as Reduce<U256>>::reduce_bytes(&public_key.point().to_affine().x());
|
||||
let sa = -(s * x_scalar);
|
||||
|
||||
69
networks/ethereum/schnorr/src/tests/public_key.rs
Normal file
69
networks/ethereum/schnorr/src/tests/public_key.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use rand_core::OsRng;
|
||||
|
||||
use subtle::Choice;
|
||||
use group::ff::{Field, PrimeField};
|
||||
use k256::{
|
||||
elliptic_curve::{
|
||||
FieldBytesEncoding,
|
||||
ops::Reduce,
|
||||
point::{AffineCoordinates, DecompressPoint},
|
||||
},
|
||||
AffinePoint, ProjectivePoint, Scalar, U256 as KU256,
|
||||
};
|
||||
|
||||
use crate::PublicKey;
|
||||
|
||||
// Generates a key usable within tests
|
||||
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||
loop {
|
||||
let key = Scalar::random(&mut OsRng);
|
||||
let point = ProjectivePoint::GENERATOR * key;
|
||||
if let Some(public_key) = PublicKey::new(point) {
|
||||
// While here, test `PublicKey::point` and its serialization functions
|
||||
assert_eq!(point, public_key.point());
|
||||
assert_eq!(PublicKey::from_eth_repr(public_key.eth_repr()).unwrap(), public_key);
|
||||
return (key, public_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_odd_key() {
|
||||
// We generate a valid key to ensure there's not some distinct reason this key is invalid
|
||||
let (_, key) = test_key();
|
||||
// We then take its point and negate it so its y-coordinate is odd
|
||||
let odd = -key.point();
|
||||
assert!(PublicKey::new(odd).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_mutual_key() {
|
||||
let mut x_coordinate = KU256::from(-(Scalar::ONE)).saturating_add(&KU256::ONE);
|
||||
|
||||
let y_is_odd = Choice::from(0);
|
||||
let non_mutual = loop {
|
||||
if let Some(point) = Option::<AffinePoint>::from(AffinePoint::decompress(
|
||||
&FieldBytesEncoding::encode_field_bytes(&x_coordinate),
|
||||
y_is_odd,
|
||||
)) {
|
||||
break point;
|
||||
}
|
||||
x_coordinate = x_coordinate.saturating_add(&KU256::ONE);
|
||||
};
|
||||
let x_coordinate = non_mutual.x();
|
||||
assert!(<Scalar as Reduce<KU256>>::reduce_bytes(&x_coordinate).to_repr() != x_coordinate);
|
||||
|
||||
// Even point whose x-coordinate isn't mutual to both fields (making it non-zero)
|
||||
assert!(PublicKey::new(non_mutual.into()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_key() {
|
||||
let y_is_odd = Choice::from(0);
|
||||
if let Some(A_affine) =
|
||||
Option::<AffinePoint>::from(AffinePoint::decompress(&[0; 32].into(), y_is_odd))
|
||||
{
|
||||
let A = ProjectivePoint::from(A_affine);
|
||||
assert!(PublicKey::new(A).is_none());
|
||||
}
|
||||
}
|
||||
33
networks/ethereum/schnorr/src/tests/signature.rs
Normal file
33
networks/ethereum/schnorr/src/tests/signature.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use rand_core::OsRng;
|
||||
|
||||
use group::ff::Field;
|
||||
use k256::Scalar;
|
||||
|
||||
use crate::Signature;
|
||||
|
||||
#[test]
|
||||
fn test_zero_challenge() {
|
||||
assert!(Signature::new(Scalar::ZERO, Scalar::random(&mut OsRng)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_serialization() {
|
||||
let c = Scalar::random(&mut OsRng);
|
||||
let s = Scalar::random(&mut OsRng);
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
assert_eq!(sig.c(), c);
|
||||
assert_eq!(sig.s(), s);
|
||||
|
||||
let sig_bytes = sig.to_bytes();
|
||||
assert_eq!(Signature::from_bytes(sig_bytes).unwrap(), sig);
|
||||
|
||||
{
|
||||
let mut sig_written_bytes = vec![];
|
||||
sig.write(&mut sig_written_bytes).unwrap();
|
||||
assert_eq!(sig_bytes.as_slice(), &sig_written_bytes);
|
||||
}
|
||||
|
||||
let mut sig_read_slice = sig_bytes.as_slice();
|
||||
assert_eq!(Signature::read(&mut sig_read_slice).unwrap(), sig);
|
||||
assert!(sig_read_slice.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user