mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-26 21:50:26 +00:00
Add tests for the premise of the Schnorr contract to the Schnorr crate
This commit is contained in:
parent
0813351f1f
commit
80ca2b780a
6 changed files with 136 additions and 85 deletions
|
@ -21,15 +21,16 @@ sha3 = { version = "0.10", default-features = false, features = ["std"] }
|
||||||
group = { version = "0.13", default-features = false, features = ["alloc"] }
|
group = { version = "0.13", default-features = false, features = ["alloc"] }
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] }
|
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] }
|
||||||
|
|
||||||
alloy-sol-types = { version = "0.8", default-features = false }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
build-solidity-contracts = { path = "../build-contracts", version = "0.1" }
|
build-solidity-contracts = { path = "../build-contracts", version = "0.1" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
k256 = { version = "^0.13.1", default-features = false, features = ["ecdsa"] }
|
||||||
|
|
||||||
alloy-core = { version = "0.8", default-features = false }
|
alloy-core = { version = "0.8", default-features = false }
|
||||||
|
alloy-sol-types = { version = "0.8", default-features = false }
|
||||||
|
|
||||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||||
|
|
|
@ -7,8 +7,9 @@ library Schnorr {
|
||||||
uint256 constant private Q =
|
uint256 constant private Q =
|
||||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
||||||
|
|
||||||
// We fix the key to have an even y coordinate to save a word when verifying
|
// We fix the key to have:
|
||||||
// signatures. This is comparable to Bitcoin Taproot's encoding of keys
|
// 1) An even y-coordinate
|
||||||
|
// 2) An x-coordinate < Q
|
||||||
uint8 constant private KEY_PARITY = 27;
|
uint8 constant private KEY_PARITY = 27;
|
||||||
|
|
||||||
// px := public key x-coordinate, where the public key has an even y-coordinate
|
// px := public key x-coordinate, where the public key has an even y-coordinate
|
||||||
|
@ -27,11 +28,17 @@ library Schnorr {
|
||||||
bytes32 sa = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
bytes32 sa = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
||||||
bytes32 ca = bytes32(Q - mulmod(uint256(c), uint256(px), Q));
|
bytes32 ca = bytes32(Q - mulmod(uint256(c), uint256(px), Q));
|
||||||
|
|
||||||
// For safety, we want each input to ecrecover to not be 0 (sa, px, ca)
|
/*
|
||||||
// The ecrecover precompile checks `r` and `s` (`px` and `ca`) are non-zero
|
The ecrecover precompile checks `r` and `s` (`px` and `ca`) are non-zero,
|
||||||
// That leaves us to check `sa` are non-zero
|
banning the two keys with zero for their x-coordinate and zero challenge.
|
||||||
if (sa == 0) return false;
|
Each has negligible probability of occuring (assuming zero x-coordinates
|
||||||
|
are even on-curve in the first place).
|
||||||
|
|
||||||
|
`sa` is not checked to be non-zero yet it does not need to be. The inverse
|
||||||
|
of it is never taken.
|
||||||
|
*/
|
||||||
address R = ecrecover(sa, KEY_PARITY, px, ca);
|
address R = ecrecover(sa, KEY_PARITY, px, ca);
|
||||||
|
// The ecrecover failed
|
||||||
if (R == address(0)) return false;
|
if (R == address(0)) return false;
|
||||||
|
|
||||||
// Check the signature is correct by rebuilding the challenge
|
// Check the signature is correct by rebuilding the challenge
|
||||||
|
|
|
@ -37,7 +37,13 @@ impl PublicKey {
|
||||||
None?;
|
None?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(PublicKey { A, x_coordinate: x_coordinate.into() })
|
let x_coordinate: [u8; 32] = x_coordinate.into();
|
||||||
|
// Returns None if the x-coordinate is 0
|
||||||
|
// Such keys will never have their signatures able to be verified
|
||||||
|
if x_coordinate == [0; 32] {
|
||||||
|
None?;
|
||||||
|
}
|
||||||
|
Some(PublicKey { A, x_coordinate })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The point for this public key.
|
/// The point for this public key.
|
||||||
|
|
|
@ -17,6 +17,8 @@ use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||||
|
|
||||||
use crate::{PublicKey, Signature};
|
use crate::{PublicKey, Signature};
|
||||||
|
|
||||||
|
mod premise;
|
||||||
|
|
||||||
#[expect(warnings)]
|
#[expect(warnings)]
|
||||||
#[expect(needless_pass_by_value)]
|
#[expect(needless_pass_by_value)]
|
||||||
#[expect(clippy::all)]
|
#[expect(clippy::all)]
|
111
networks/ethereum/schnorr/src/tests/premise.rs
Normal file
111
networks/ethereum/schnorr/src/tests/premise.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
|
use sha3::{Digest, Keccak256};
|
||||||
|
use group::ff::{Field, PrimeField};
|
||||||
|
use k256::{
|
||||||
|
elliptic_curve::{ops::Reduce, point::AffineCoordinates, sec1::ToEncodedPoint},
|
||||||
|
ecdsa::{
|
||||||
|
self, hazmat::SignPrimitive, signature::hazmat::PrehashVerifier, SigningKey, VerifyingKey,
|
||||||
|
},
|
||||||
|
U256, Scalar, ProjectivePoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
use alloy_core::primitives::Address;
|
||||||
|
|
||||||
|
use crate::{PublicKey, Signature};
|
||||||
|
|
||||||
|
// 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]> {
|
||||||
|
let sig = ecdsa::Signature::from_scalars(r, s).ok()?;
|
||||||
|
let message: [u8; 32] = message.to_repr().into();
|
||||||
|
alloy_core::primitives::Signature::from_signature_and_parity(
|
||||||
|
sig,
|
||||||
|
alloy_core::primitives::Parity::Parity(odd_y),
|
||||||
|
)
|
||||||
|
.ok()?
|
||||||
|
.recover_address_from_prehash(&alloy_core::primitives::B256::from(message))
|
||||||
|
.ok()
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ecrecover behaves as expected
|
||||||
|
#[test]
|
||||||
|
fn test_ecrecover() {
|
||||||
|
let private = SigningKey::random(&mut OsRng);
|
||||||
|
let public = VerifyingKey::from(&private);
|
||||||
|
|
||||||
|
// Sign the signature
|
||||||
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
|
let (sig, recovery_id) = private
|
||||||
|
.as_nonzero_scalar()
|
||||||
|
.try_sign_prehashed(Scalar::random(&mut OsRng), &Keccak256::digest(MESSAGE))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Sanity check the signature verifies
|
||||||
|
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
||||||
|
{
|
||||||
|
assert_eq!(public.verify_prehash(&Keccak256::digest(MESSAGE), &sig).unwrap(), ());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the ecrecover
|
||||||
|
assert_eq!(
|
||||||
|
ecrecover(
|
||||||
|
<Scalar as Reduce<U256>>::reduce_bytes(&Keccak256::digest(MESSAGE)),
|
||||||
|
u8::from(recovery_id.unwrap().is_y_odd()) == 1,
|
||||||
|
*sig.r(),
|
||||||
|
*sig.s()
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Address::from_raw_public_key(&public.to_encoded_point(false).as_ref()[1 ..]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that we can recover the nonce from a Schnorr signature via a call to ecrecover, the premise
|
||||||
|
// 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 nonce = Scalar::random(&mut OsRng);
|
||||||
|
let R = ProjectivePoint::GENERATOR * nonce;
|
||||||
|
|
||||||
|
let mut message = vec![0; 1 + usize::try_from(OsRng.next_u32() % 256).unwrap()];
|
||||||
|
OsRng.fill_bytes(&mut message);
|
||||||
|
|
||||||
|
let c = Signature::challenge(R, &public_key, &message);
|
||||||
|
let s = nonce + (c * key);
|
||||||
|
|
||||||
|
/*
|
||||||
|
An ECDSA signature is `(r, s)` with `s = (H(m) + rx) / k`, where:
|
||||||
|
- `m` is 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`.
|
||||||
|
|
||||||
|
We want to calculate `R` from `(c, s)` where `s = r + cx`. That means we need to calculate
|
||||||
|
`sG - cX`.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
*/
|
||||||
|
let x_scalar = <Scalar as Reduce<U256>>::reduce_bytes(&public_key.point().to_affine().x());
|
||||||
|
let sa = -(s * x_scalar);
|
||||||
|
let ca = -(c * x_scalar);
|
||||||
|
|
||||||
|
let q = ecrecover(sa, false, x_scalar, ca).unwrap();
|
||||||
|
assert_eq!(q, Address::from_raw_public_key(&R.to_encoded_point(false).as_ref()[1 ..]));
|
||||||
|
}
|
|
@ -16,54 +16,6 @@ use frost::{
|
||||||
|
|
||||||
use crate::{crypto::*, tests::key_gen};
|
use crate::{crypto::*, tests::key_gen};
|
||||||
|
|
||||||
// The ecrecover opcode, yet with parity replacing v
|
|
||||||
pub(crate) fn ecrecover(message: Scalar, odd_y: bool, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
|
||||||
let sig = ecdsa::Signature::from_scalars(r, s).ok()?;
|
|
||||||
let message: [u8; 32] = message.to_repr().into();
|
|
||||||
alloy_core::primitives::Signature::from_signature_and_parity(
|
|
||||||
sig,
|
|
||||||
alloy_core::primitives::Parity::Parity(odd_y),
|
|
||||||
)
|
|
||||||
.ok()?
|
|
||||||
.recover_address_from_prehash(&alloy_core::primitives::B256::from(message))
|
|
||||||
.ok()
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover() {
|
|
||||||
let private = SigningKey::random(&mut OsRng);
|
|
||||||
let public = VerifyingKey::from(&private);
|
|
||||||
|
|
||||||
// Sign the signature
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let (sig, recovery_id) = private
|
|
||||||
.as_nonzero_scalar()
|
|
||||||
.try_sign_prehashed(
|
|
||||||
<Secp256k1 as Ciphersuite>::F::random(&mut OsRng),
|
|
||||||
&keccak256(MESSAGE).into(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Sanity check the signature verifies
|
|
||||||
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
|
||||||
{
|
|
||||||
assert_eq!(public.verify_prehash(&keccak256(MESSAGE), &sig).unwrap(), ());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the ecrecover
|
|
||||||
assert_eq!(
|
|
||||||
ecrecover(
|
|
||||||
hash_to_scalar(MESSAGE),
|
|
||||||
u8::from(recovery_id.unwrap().is_y_odd()) == 1,
|
|
||||||
*sig.r(),
|
|
||||||
*sig.s()
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
address(&ProjectivePoint::from(public.as_affine()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the sign test with the EthereumHram
|
// Run the sign test with the EthereumHram
|
||||||
#[test]
|
#[test]
|
||||||
fn test_signing() {
|
fn test_signing() {
|
||||||
|
@ -75,31 +27,3 @@ fn test_signing() {
|
||||||
let _sig =
|
let _sig =
|
||||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn preprocess_signature_for_ecrecover(
|
|
||||||
R: ProjectivePoint,
|
|
||||||
public_key: &PublicKey,
|
|
||||||
m: &[u8],
|
|
||||||
s: Scalar,
|
|
||||||
) -> (Scalar, Scalar) {
|
|
||||||
let c = EthereumHram::hram(&R, &public_key.A, m);
|
|
||||||
let sa = -(s * public_key.px);
|
|
||||||
let ca = -(c * public_key.px);
|
|
||||||
(sa, ca)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover_hack() {
|
|
||||||
let (keys, public_key) = key_gen();
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let sig =
|
|
||||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
|
||||||
|
|
||||||
let (sa, ca) = preprocess_signature_for_ecrecover(sig.R, &public_key, MESSAGE, sig.s);
|
|
||||||
let q = ecrecover(sa, false, public_key.px, ca).unwrap();
|
|
||||||
assert_eq!(q, address(&sig.R));
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue