mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 05:59:23 +00:00
Add tests for the premise of the Schnorr contract to the Schnorr crate
This commit is contained in:
112
networks/ethereum/schnorr/src/tests/mod.rs
Normal file
112
networks/ethereum/schnorr/src/tests/mod.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use group::ff::{Field, PrimeField};
|
||||
use k256::{Scalar, ProjectivePoint};
|
||||
|
||||
use alloy_core::primitives::Address;
|
||||
use alloy_sol_types::SolCall;
|
||||
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||
|
||||
use crate::{PublicKey, Signature};
|
||||
|
||||
mod premise;
|
||||
|
||||
#[expect(warnings)]
|
||||
#[expect(needless_pass_by_value)]
|
||||
#[expect(clippy::all)]
|
||||
#[expect(clippy::ignored_unit_patterns)]
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
mod abi {
|
||||
alloy_sol_types::sol!("contracts/tests/Schnorr.sol");
|
||||
pub(crate) use TestSchnorr::*;
|
||||
}
|
||||
|
||||
async fn setup_test() -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Address) {
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
let provider = Arc::new(RootProvider::new(
|
||||
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
||||
));
|
||||
|
||||
let mut address = [0; 20];
|
||||
OsRng.fill_bytes(&mut address);
|
||||
let address = Address::from(address);
|
||||
let _: () = provider
|
||||
.raw_request(
|
||||
"anvil_setCode".into(),
|
||||
[
|
||||
address.to_string(),
|
||||
include_str!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/ethereum-schnorr-contract/TestSchnorr.bin-runtime"
|
||||
))
|
||||
.to_string(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(anvil, provider, address)
|
||||
}
|
||||
|
||||
async fn call_verify(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
address: Address,
|
||||
public_key: &PublicKey,
|
||||
message: &[u8],
|
||||
signature: &Signature,
|
||||
) -> bool {
|
||||
let public_key: [u8; 32] = public_key.eth_repr();
|
||||
let c_bytes: [u8; 32] = signature.c().to_repr().into();
|
||||
let s_bytes: [u8; 32] = signature.s().to_repr().into();
|
||||
let call = TransactionRequest::default().to(address).input(TransactionInput::new(
|
||||
abi::verifyCall::new((
|
||||
public_key.into(),
|
||||
message.to_vec().into(),
|
||||
c_bytes.into(),
|
||||
s_bytes.into(),
|
||||
))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
));
|
||||
let bytes = provider.call(&call).await.unwrap();
|
||||
let res = abi::verifyCall::abi_decode_returns(&bytes, true).unwrap();
|
||||
|
||||
res._0
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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 nonce = Scalar::random(&mut OsRng);
|
||||
let mut message = vec![0; 1 + usize::try_from(OsRng.next_u32() % 256).unwrap()];
|
||||
OsRng.fill_bytes(&mut message);
|
||||
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &public_key, &message);
|
||||
let s = nonce + (c * key);
|
||||
|
||||
let sig = Signature::new(c, s);
|
||||
assert!(sig.verify(&public_key, &message));
|
||||
assert!(call_verify(&provider, address, &public_key, &message, &sig).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);
|
||||
}
|
||||
}
|
||||
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 ..]));
|
||||
}
|
||||
Reference in New Issue
Block a user