From 1c5bc2259e609890d852ca8291980b9addbb001c Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 15 Sep 2024 00:41:16 -0400 Subject: [PATCH] Dedicated crate for the Schnorr contract --- .github/workflows/networks-tests.yml | 1 + Cargo.lock | 20 ++++ Cargo.toml | 1 + deny.toml | 1 + networks/ethereum/build-contracts/src/lib.rs | 2 +- networks/ethereum/schnorr/.gitignore | 1 + networks/ethereum/schnorr/Cargo.toml | 42 +++++++ networks/ethereum/schnorr/LICENSE | 15 +++ networks/ethereum/schnorr/README.md | 5 + networks/ethereum/schnorr/build.rs | 3 + .../ethereum/schnorr}/contracts/Schnorr.sol | 32 +++--- .../schnorr}/contracts/tests/Schnorr.sol | 8 +- networks/ethereum/schnorr/src/lib.rs | 15 +++ networks/ethereum/schnorr/src/public_key.rs | 68 ++++++++++++ networks/ethereum/schnorr/src/signature.rs | 95 ++++++++++++++++ networks/ethereum/schnorr/src/tests.rs | 103 ++++++++++++++++++ processor/ethereum/contracts/.gitignore | 2 - .../ethereum/ethereum-serai/src/crypto.rs | 102 ----------------- .../ethereum/ethereum-serai/src/tests/mod.rs | 2 - .../ethereum-serai/src/tests/schnorr.rs | 93 ---------------- 20 files changed, 389 insertions(+), 222 deletions(-) create mode 100644 networks/ethereum/schnorr/.gitignore create mode 100644 networks/ethereum/schnorr/Cargo.toml create mode 100644 networks/ethereum/schnorr/LICENSE create mode 100644 networks/ethereum/schnorr/README.md create mode 100644 networks/ethereum/schnorr/build.rs rename {processor/ethereum/contracts => networks/ethereum/schnorr}/contracts/Schnorr.sol (50%) rename {processor/ethereum/contracts => networks/ethereum/schnorr}/contracts/tests/Schnorr.sol (53%) create mode 100644 networks/ethereum/schnorr/src/lib.rs create mode 100644 networks/ethereum/schnorr/src/public_key.rs create mode 100644 networks/ethereum/schnorr/src/signature.rs create mode 100644 networks/ethereum/schnorr/src/tests.rs delete mode 100644 processor/ethereum/ethereum-serai/src/tests/schnorr.rs diff --git a/.github/workflows/networks-tests.yml b/.github/workflows/networks-tests.yml index ee095df6..92044978 100644 --- a/.github/workflows/networks-tests.yml +++ b/.github/workflows/networks-tests.yml @@ -31,6 +31,7 @@ jobs: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \ -p bitcoin-serai \ -p build-solidity-contracts \ + -p ethereum-schnorr-contract \ -p alloy-simple-request-transport \ -p serai-ethereum-relayer \ -p monero-io \ diff --git a/Cargo.lock b/Cargo.lock index f4584f65..353206e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2483,6 +2483,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ethereum-schnorr-contract" +version = "0.1.0" +dependencies = [ + "alloy-core", + "alloy-node-bindings", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-eth", + "alloy-simple-request-transport", + "alloy-sol-types", + "build-solidity-contracts", + "group", + "k256", + "rand_core", + "sha3", + "subtle", + "tokio", +] + [[package]] name = "ethereum-serai" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 08e0aabe..b30112b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "networks/bitcoin", "networks/ethereum/build-contracts", + "networks/ethereum/schnorr", "networks/ethereum/alloy-simple-request-transport", "networks/ethereum/relayer", diff --git a/deny.toml b/deny.toml index cef3a683..ec948fef 100644 --- a/deny.toml +++ b/deny.toml @@ -40,6 +40,7 @@ allow = [ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-env" }, + { allow = ["AGPL-3.0"], name = "ethereum-schnorr-contract" }, { allow = ["AGPL-3.0"], name = "serai-ethereum-relayer" }, { allow = ["AGPL-3.0"], name = "serai-message-queue" }, diff --git a/networks/ethereum/build-contracts/src/lib.rs b/networks/ethereum/build-contracts/src/lib.rs index c546b111..93ab253e 100644 --- a/networks/ethereum/build-contracts/src/lib.rs +++ b/networks/ethereum/build-contracts/src/lib.rs @@ -34,7 +34,7 @@ pub fn build(contracts_path: &str, artifacts_path: &str) -> Result<(), String> { let args = [ "--base-path", ".", "-o", "./artifacts", "--overwrite", - "--bin", "--abi", + "--bin", "--bin-runtime", "--abi", "--via-ir", "--optimize", "--no-color", ]; diff --git a/networks/ethereum/schnorr/.gitignore b/networks/ethereum/schnorr/.gitignore new file mode 100644 index 00000000..de153db3 --- /dev/null +++ b/networks/ethereum/schnorr/.gitignore @@ -0,0 +1 @@ +artifacts diff --git a/networks/ethereum/schnorr/Cargo.toml b/networks/ethereum/schnorr/Cargo.toml new file mode 100644 index 00000000..1c5d4f02 --- /dev/null +++ b/networks/ethereum/schnorr/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "ethereum-schnorr-contract" +version = "0.1.0" +description = "A Solidity contract to verify Schnorr signatures" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/ethereum/schnorr" +authors = ["Luke Parker ", "Elizabeth Binks "] +edition = "2021" +publish = false +rust-version = "1.79" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +subtle = { version = "2", default-features = false, features = ["std"] } +sha3 = { version = "0.10", default-features = false, features = ["std"] } +group = { version = "0.13", default-features = false, features = ["alloc"] } +k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] } + +alloy-sol-types = { version = "0.8", default-features = false } + +[build-dependencies] +build-solidity-contracts = { path = "../build-contracts", version = "0.1" } + +[dev-dependencies] +rand_core = { version = "0.6", default-features = false, features = ["std"] } + +alloy-core = { version = "0.8", 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-client = { version = "0.3", default-features = false } +alloy-provider = { version = "0.3", default-features = false } + +alloy-node-bindings = { version = "0.3", default-features = false } + +tokio = { version = "1", default-features = false, features = ["macros"] } diff --git a/networks/ethereum/schnorr/LICENSE b/networks/ethereum/schnorr/LICENSE new file mode 100644 index 00000000..41d5a261 --- /dev/null +++ b/networks/ethereum/schnorr/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2024 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/networks/ethereum/schnorr/README.md b/networks/ethereum/schnorr/README.md new file mode 100644 index 00000000..410cf520 --- /dev/null +++ b/networks/ethereum/schnorr/README.md @@ -0,0 +1,5 @@ +# Ethereum Schnorr Contract + +An Ethereum contract to verify Schnorr signatures. + +This crate will fail to build if `solc` is not installed and available. diff --git a/networks/ethereum/schnorr/build.rs b/networks/ethereum/schnorr/build.rs new file mode 100644 index 00000000..8e310b60 --- /dev/null +++ b/networks/ethereum/schnorr/build.rs @@ -0,0 +1,3 @@ +fn main() { + build_solidity_contracts::build("contracts", "artifacts").unwrap(); +} diff --git a/processor/ethereum/contracts/contracts/Schnorr.sol b/networks/ethereum/schnorr/contracts/Schnorr.sol similarity index 50% rename from processor/ethereum/contracts/contracts/Schnorr.sol rename to networks/ethereum/schnorr/contracts/Schnorr.sol index 8edcdffd..1c39c6d7 100644 --- a/processor/ethereum/contracts/contracts/Schnorr.sol +++ b/networks/ethereum/schnorr/contracts/Schnorr.sol @@ -1,24 +1,20 @@ -// SPDX-License-Identifier: AGPLv3 +// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -// see https://github.com/noot/schnorr-verify for implementation details +// See https://github.com/noot/schnorr-verify for implementation details library Schnorr { // secp256k1 group order - uint256 constant public Q = + uint256 constant private Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; - // Fixed parity for the public keys used in this contract - // This avoids spending a word passing the parity in a similar style to - // Bitcoin's Taproot - uint8 constant public KEY_PARITY = 27; + // We fix the key to have an even y coordinate to save a word when verifying + // signatures. This is comparable to Bitcoin Taproot's encoding of keys + uint8 constant private KEY_PARITY = 27; - error InvalidSOrA(); - error MalformedSignature(); - - // px := public key x-coord, where the public key has a parity of KEY_PARITY - // message := 32-byte hash of the message - // c := schnorr signature challenge - // s := schnorr signature + // px := public key x-coordinate, where the public key has an even y-coordinate + // message := the message signed + // c := Schnorr signature challenge + // s := Schnorr signature solution function verify( bytes32 px, bytes memory message, @@ -31,12 +27,12 @@ library Schnorr { bytes32 sa = bytes32(Q - mulmod(uint256(s), uint256(px), Q)); bytes32 ca = bytes32(Q - mulmod(uint256(c), uint256(px), Q)); - // For safety, we want each input to ecrecover to be 0 (sa, px, ca) - // The ecreover precomple checks `r` and `s` (`px` and `ca`) are non-zero + // 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 // That leaves us to check `sa` are non-zero - if (sa == 0) revert InvalidSOrA(); + if (sa == 0) return false; address R = ecrecover(sa, KEY_PARITY, px, ca); - if (R == address(0)) revert MalformedSignature(); + if (R == address(0)) return false; // Check the signature is correct by rebuilding the challenge return c == keccak256(abi.encodePacked(R, px, message)); diff --git a/processor/ethereum/contracts/contracts/tests/Schnorr.sol b/networks/ethereum/schnorr/contracts/tests/Schnorr.sol similarity index 53% rename from processor/ethereum/contracts/contracts/tests/Schnorr.sol rename to networks/ethereum/schnorr/contracts/tests/Schnorr.sol index 832cd2fe..18a58cf9 100644 --- a/processor/ethereum/contracts/contracts/tests/Schnorr.sol +++ b/networks/ethereum/schnorr/contracts/tests/Schnorr.sol @@ -1,15 +1,15 @@ -// SPDX-License-Identifier: AGPLv3 +// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import "../../../contracts/Schnorr.sol"; +import "../Schnorr.sol"; contract TestSchnorr { function verify( - bytes32 px, + bytes32 public_key, bytes calldata message, bytes32 c, bytes32 s ) external pure returns (bool) { - return Schnorr.verify(px, message, c, s); + return Schnorr.verify(public_key, message, c, s); } } diff --git a/networks/ethereum/schnorr/src/lib.rs b/networks/ethereum/schnorr/src/lib.rs new file mode 100644 index 00000000..79e2e094 --- /dev/null +++ b/networks/ethereum/schnorr/src/lib.rs @@ -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; diff --git a/networks/ethereum/schnorr/src/public_key.rs b/networks/ethereum/schnorr/src/public_key.rs new file mode 100644 index 00000000..b0cc04df --- /dev/null +++ b/networks/ethereum/schnorr/src/public_key.rs @@ -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 { + 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 >::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 { + let x_coordinate = repr; + + let y_is_odd = Choice::from(0); + let A_affine = + Option::::from(AffinePoint::decompress(&x_coordinate.into(), y_is_odd))?; + let A = ProjectivePoint::from(A_affine); + Some(PublicKey { A, x_coordinate }) + } +} diff --git a/networks/ethereum/schnorr/src/signature.rs b/networks/ethereum/schnorr/src/signature.rs new file mode 100644 index 00000000..cd467cea --- /dev/null +++ b/networks/ethereum/schnorr/src/signature.rs @@ -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); + >::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 { + let mut read_F = || -> io::Result { + let mut bytes = [0; 32]; + reader.read_exact(&mut bytes)?; + Option::::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::read(&mut bytes.as_slice()) + } +} diff --git a/networks/ethereum/schnorr/src/tests.rs b/networks/ethereum/schnorr/src/tests.rs new file mode 100644 index 00000000..1c3509cc --- /dev/null +++ b/networks/ethereum/schnorr/src/tests.rs @@ -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>, 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, + 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); + } +} diff --git a/processor/ethereum/contracts/.gitignore b/processor/ethereum/contracts/.gitignore index 2dccdce9..de153db3 100644 --- a/processor/ethereum/contracts/.gitignore +++ b/processor/ethereum/contracts/.gitignore @@ -1,3 +1 @@ -# Solidity build outputs -cache artifacts diff --git a/processor/ethereum/ethereum-serai/src/crypto.rs b/processor/ethereum/ethereum-serai/src/crypto.rs index 326343d8..3366b744 100644 --- a/processor/ethereum/ethereum-serai/src/crypto.rs +++ b/processor/ethereum/ethereum-serai/src/crypto.rs @@ -62,56 +62,6 @@ pub fn deterministically_sign(tx: &TxLegacy) -> Signed { } } -/// The public key for a Schnorr-signing account. -#[allow(non_snake_case)] -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct PublicKey { - pub(crate) A: ProjectivePoint, - pub(crate) px: Scalar, -} - -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). - #[allow(non_snake_case)] - pub fn new(A: ProjectivePoint) -> Option { - let affine = A.to_affine(); - // Only allow even keys to save a word within Ethereum - let is_odd = bool::from(affine.y_is_odd()); - if is_odd { - None?; - } - - let x_coord = affine.x(); - let x_coord_scalar = >::reduce_bytes(&x_coord); - // Return None if a reduction would occur - // Reductions would be incredibly unlikely and shouldn't be an issue, yet it's one less - // headache/concern to have - // This does ban a trivial amoount of public keys - if x_coord_scalar.to_repr() != x_coord { - None?; - } - - Some(PublicKey { A, px: x_coord_scalar }) - } - - pub fn point(&self) -> ProjectivePoint { - self.A - } - - pub fn eth_repr(&self) -> [u8; 32] { - self.px.to_repr().into() - } - - pub fn from_eth_repr(repr: [u8; 32]) -> Option { - #[allow(non_snake_case)] - let A = Option::::from(AffinePoint::decompress(&repr.into(), 0.into()))?.into(); - Option::from(Scalar::from_repr(repr.into())).map(|px| PublicKey { A, px }) - } -} - /// The HRAm to use for the Schnorr contract. #[derive(Clone, Default)] pub struct EthereumHram {} @@ -128,58 +78,6 @@ impl Hram for EthereumHram { } } -/// A signature for the Schnorr contract. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Signature { - pub(crate) c: Scalar, - pub(crate) s: Scalar, -} -impl Signature { - pub fn verify(&self, public_key: &PublicKey, message: &[u8]) -> bool { - #[allow(non_snake_case)] - let R = (Secp256k1::generator() * self.s) - (public_key.A * self.c); - EthereumHram::hram(&R, &public_key.A, message) == self.c - } - - /// Construct a new `Signature`. - /// - /// This will return None if the signature is invalid. - pub fn new( - public_key: &PublicKey, - message: &[u8], - signature: SchnorrSignature, - ) -> Option { - let c = EthereumHram::hram(&signature.R, &public_key.A, message); - if !signature.verify(public_key.A, c) { - None?; - } - - let res = Signature { c, s: signature.s }; - assert!(res.verify(public_key, message)); - Some(res) - } - - pub fn c(&self) -> Scalar { - self.c - } - pub fn s(&self) -> Scalar { - self.s - } - - 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 - } - - pub fn from_bytes(bytes: [u8; 64]) -> std::io::Result { - let mut reader = bytes.as_slice(); - let c = Secp256k1::read_F(&mut reader)?; - let s = Secp256k1::read_F(&mut reader)?; - Ok(Signature { c, s }) - } -} impl From<&Signature> for AbiSignature { fn from(sig: &Signature) -> AbiSignature { let c: [u8; 32] = sig.c.to_repr().into(); diff --git a/processor/ethereum/ethereum-serai/src/tests/mod.rs b/processor/ethereum/ethereum-serai/src/tests/mod.rs index bdfa8414..91b03d9b 100644 --- a/processor/ethereum/ethereum-serai/src/tests/mod.rs +++ b/processor/ethereum/ethereum-serai/src/tests/mod.rs @@ -23,8 +23,6 @@ mod crypto; #[cfg(test)] use contracts::tests as abi; #[cfg(test)] -mod schnorr; -#[cfg(test)] mod router; pub fn key_gen() -> (HashMap>, PublicKey) { diff --git a/processor/ethereum/ethereum-serai/src/tests/schnorr.rs b/processor/ethereum/ethereum-serai/src/tests/schnorr.rs deleted file mode 100644 index 2c72ed19..00000000 --- a/processor/ethereum/ethereum-serai/src/tests/schnorr.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::sync::Arc; - -use rand_core::OsRng; - -use group::ff::PrimeField; -use k256::Scalar; - -use frost::{ - curve::Secp256k1, - algorithm::IetfSchnorr, - tests::{algorithm_machines, sign}, -}; - -use alloy_core::primitives::Address; - -use alloy_sol_types::SolCall; - -use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; -use alloy_simple_request_transport::SimpleRequest; -use alloy_rpc_client::ClientBuilder; -use alloy_provider::{Provider, RootProvider}; - -use alloy_node_bindings::{Anvil, AnvilInstance}; - -use crate::{ - Error, - crypto::*, - tests::{key_gen, deploy_contract, abi::schnorr as abi}, -}; - -async fn setup_test() -> (AnvilInstance, Arc>, Address) { - let anvil = Anvil::new().spawn(); - - let provider = RootProvider::new( - ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true), - ); - let wallet = anvil.keys()[0].clone().into(); - let client = Arc::new(provider); - - let address = deploy_contract(client.clone(), &wallet, "TestSchnorr").await.unwrap(); - (anvil, client, address) -} - -#[tokio::test] -async fn test_deploy_contract() { - setup_test().await; -} - -pub async fn call_verify( - provider: &RootProvider, - contract: Address, - public_key: &PublicKey, - message: &[u8], - signature: &Signature, -) -> Result<(), Error> { - let px: [u8; 32] = public_key.px.to_repr().into(); - 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(contract).input(TransactionInput::new( - abi::verifyCall::new((px.into(), message.to_vec().into(), c_bytes.into(), s_bytes.into())) - .abi_encode() - .into(), - )); - let bytes = provider.call(&call).await.map_err(|_| Error::ConnectionError)?; - let res = - abi::verifyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?; - - if res._0 { - Ok(()) - } else { - Err(Error::InvalidSignature) - } -} - -#[tokio::test] -async fn test_ecrecover_hack() { - let (_anvil, client, contract) = setup_test().await; - - let (keys, public_key) = key_gen(); - - const MESSAGE: &[u8] = b"Hello, World!"; - - let algo = IetfSchnorr::::ietf(); - let sig = - sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE); - let sig = Signature::new(&public_key, MESSAGE, sig).unwrap(); - - call_verify(&client, contract, &public_key, MESSAGE, &sig).await.unwrap(); - // Test an invalid signature fails - let mut sig = sig; - sig.s += Scalar::ONE; - assert!(call_verify(&client, contract, &public_key, MESSAGE, &sig).await.is_err()); -}