mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 12:49:23 +00:00
Dedicated crate for the Schnorr contract
This commit is contained in:
15
networks/ethereum/schnorr/src/lib.rs
Normal file
15
networks/ethereum/schnorr/src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
/// The initialization bytecode of the Schnorr library.
|
||||
pub const INIT_BYTECODE: &str = include_str!("../artifacts/Schnorr.bin");
|
||||
|
||||
mod public_key;
|
||||
pub use public_key::PublicKey;
|
||||
mod signature;
|
||||
pub use signature::Signature;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
68
networks/ethereum/schnorr/src/public_key.rs
Normal file
68
networks/ethereum/schnorr/src/public_key.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use subtle::Choice;
|
||||
use group::ff::PrimeField;
|
||||
use k256::{
|
||||
elliptic_curve::{
|
||||
ops::Reduce,
|
||||
point::{AffineCoordinates, DecompressPoint},
|
||||
},
|
||||
AffinePoint, ProjectivePoint, Scalar, U256 as KU256,
|
||||
};
|
||||
|
||||
/// A public key for the Schnorr Solidity library.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct PublicKey {
|
||||
A: ProjectivePoint,
|
||||
x_coordinate: [u8; 32],
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// Construct a new `PublicKey`.
|
||||
///
|
||||
/// This will return None if the provided point isn't eligible to be a public key (due to
|
||||
/// bounds such as parity).
|
||||
#[must_use]
|
||||
pub fn new(A: ProjectivePoint) -> Option<PublicKey> {
|
||||
let affine = A.to_affine();
|
||||
|
||||
// Only allow even keys to save a word within Ethereum
|
||||
if bool::from(affine.y_is_odd()) {
|
||||
None?;
|
||||
}
|
||||
|
||||
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
|
||||
if <Scalar as Reduce<KU256>>::reduce_bytes(&x_coordinate).to_repr() != x_coordinate {
|
||||
None?;
|
||||
}
|
||||
|
||||
Some(PublicKey { A, x_coordinate: x_coordinate.into() })
|
||||
}
|
||||
|
||||
/// The point for this public key.
|
||||
#[must_use]
|
||||
pub fn point(&self) -> ProjectivePoint {
|
||||
self.A
|
||||
}
|
||||
|
||||
/// The Ethereum representation of this public key.
|
||||
#[must_use]
|
||||
pub fn eth_repr(&self) -> [u8; 32] {
|
||||
// We only encode the x-coordinate due to fixing the sign of the y-coordinate
|
||||
self.x_coordinate
|
||||
}
|
||||
|
||||
/// Construct a PublicKey from its Ethereum representation.
|
||||
// This wouldn't be possible if the x-coordinate had been reduced
|
||||
#[must_use]
|
||||
pub fn from_eth_repr(repr: [u8; 32]) -> Option<Self> {
|
||||
let x_coordinate = repr;
|
||||
|
||||
let y_is_odd = Choice::from(0);
|
||||
let A_affine =
|
||||
Option::<AffinePoint>::from(AffinePoint::decompress(&x_coordinate.into(), y_is_odd))?;
|
||||
let A = ProjectivePoint::from(A_affine);
|
||||
Some(PublicKey { A, x_coordinate })
|
||||
}
|
||||
}
|
||||
95
networks/ethereum/schnorr/src/signature.rs
Normal file
95
networks/ethereum/schnorr/src/signature.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::io;
|
||||
|
||||
use sha3::{Digest, Keccak256};
|
||||
|
||||
use group::ff::PrimeField;
|
||||
use k256::{
|
||||
elliptic_curve::{ops::Reduce, sec1::ToEncodedPoint},
|
||||
ProjectivePoint, Scalar, U256 as KU256,
|
||||
};
|
||||
|
||||
use crate::PublicKey;
|
||||
|
||||
/// A signature for the Schnorr Solidity library.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Signature {
|
||||
c: Scalar,
|
||||
s: Scalar,
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
/// Construct a new `Signature`.
|
||||
#[must_use]
|
||||
pub fn new(c: Scalar, s: Scalar) -> Signature {
|
||||
Signature { c, s }
|
||||
}
|
||||
|
||||
/// The challenge for a signature.
|
||||
#[must_use]
|
||||
pub fn challenge(R: ProjectivePoint, key: &PublicKey, message: &[u8]) -> Scalar {
|
||||
// H(R || A || m)
|
||||
let mut hash = Keccak256::new();
|
||||
// We transcript the nonce as an address since ecrecover yields an address
|
||||
hash.update({
|
||||
let uncompressed_encoded_point = R.to_encoded_point(false);
|
||||
// Skip the prefix byte marking this as uncompressed
|
||||
let x_and_y_coordinates = &uncompressed_encoded_point.as_ref()[1 ..];
|
||||
// Last 20 bytes of the hash of the x and y coordinates
|
||||
&Keccak256::digest(x_and_y_coordinates)[12 ..]
|
||||
});
|
||||
hash.update(key.eth_repr());
|
||||
hash.update(message);
|
||||
<Scalar as Reduce<KU256>>::reduce_bytes(&hash.finalize())
|
||||
}
|
||||
|
||||
/// Verify a signature.
|
||||
#[must_use]
|
||||
pub fn verify(&self, key: &PublicKey, message: &[u8]) -> bool {
|
||||
// Recover the nonce
|
||||
let R = (ProjectivePoint::GENERATOR * self.s) - (key.point() * self.c);
|
||||
// Check the challenge
|
||||
Self::challenge(R, key, message) == self.c
|
||||
}
|
||||
|
||||
/// The challenge present within this signature.
|
||||
pub fn c(&self) -> Scalar {
|
||||
self.c
|
||||
}
|
||||
|
||||
/// The signature solution present within this signature.
|
||||
pub fn s(&self) -> Scalar {
|
||||
self.s
|
||||
}
|
||||
|
||||
/// Convert the signature to bytes.
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> [u8; 64] {
|
||||
let mut res = [0; 64];
|
||||
res[.. 32].copy_from_slice(self.c.to_repr().as_ref());
|
||||
res[32 ..].copy_from_slice(self.s.to_repr().as_ref());
|
||||
res
|
||||
}
|
||||
|
||||
/// Write the signature.
|
||||
pub fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||
writer.write_all(&self.to_bytes())
|
||||
}
|
||||
|
||||
/// Read a signature.
|
||||
pub fn read(reader: &mut impl io::Read) -> io::Result<Self> {
|
||||
let mut read_F = || -> io::Result<Scalar> {
|
||||
let mut bytes = [0; 32];
|
||||
reader.read_exact(&mut bytes)?;
|
||||
Option::<Scalar>::from(Scalar::from_repr(bytes.into()))
|
||||
.ok_or_else(|| io::Error::other("invalid scalar"))
|
||||
};
|
||||
let c = read_F()?;
|
||||
let s = read_F()?;
|
||||
Ok(Signature { c, s })
|
||||
}
|
||||
|
||||
/// Read a signature from bytes.
|
||||
pub fn from_bytes(bytes: [u8; 64]) -> io::Result<Self> {
|
||||
Self::read(&mut bytes.as_slice())
|
||||
}
|
||||
}
|
||||
103
networks/ethereum/schnorr/src/tests.rs
Normal file
103
networks/ethereum/schnorr/src/tests.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
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};
|
||||
|
||||
#[allow(warnings)]
|
||||
#[allow(needless_pass_by_value)]
|
||||
#[allow(clippy::all)]
|
||||
#[allow(clippy::ignored_unit_patterns)]
|
||||
#[allow(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!("../artifacts/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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user