mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-26 05:29:55 +00:00
Dedicated crate for the Schnorr contract
This commit is contained in:
parent
bdf89f5350
commit
1c5bc2259e
20 changed files with 389 additions and 222 deletions
1
.github/workflows/networks-tests.yml
vendored
1
.github/workflows/networks-tests.yml
vendored
|
@ -31,6 +31,7 @@ jobs:
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||||
-p bitcoin-serai \
|
-p bitcoin-serai \
|
||||||
-p build-solidity-contracts \
|
-p build-solidity-contracts \
|
||||||
|
-p ethereum-schnorr-contract \
|
||||||
-p alloy-simple-request-transport \
|
-p alloy-simple-request-transport \
|
||||||
-p serai-ethereum-relayer \
|
-p serai-ethereum-relayer \
|
||||||
-p monero-io \
|
-p monero-io \
|
||||||
|
|
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -2483,6 +2483,26 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "ethereum-serai"
|
name = "ethereum-serai"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -47,6 +47,7 @@ members = [
|
||||||
"networks/bitcoin",
|
"networks/bitcoin",
|
||||||
|
|
||||||
"networks/ethereum/build-contracts",
|
"networks/ethereum/build-contracts",
|
||||||
|
"networks/ethereum/schnorr",
|
||||||
"networks/ethereum/alloy-simple-request-transport",
|
"networks/ethereum/alloy-simple-request-transport",
|
||||||
"networks/ethereum/relayer",
|
"networks/ethereum/relayer",
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ allow = [
|
||||||
exceptions = [
|
exceptions = [
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-env" },
|
{ 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-ethereum-relayer" },
|
||||||
|
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-message-queue" },
|
{ allow = ["AGPL-3.0"], name = "serai-message-queue" },
|
||||||
|
|
|
@ -34,7 +34,7 @@ pub fn build(contracts_path: &str, artifacts_path: &str) -> Result<(), String> {
|
||||||
let args = [
|
let args = [
|
||||||
"--base-path", ".",
|
"--base-path", ".",
|
||||||
"-o", "./artifacts", "--overwrite",
|
"-o", "./artifacts", "--overwrite",
|
||||||
"--bin", "--abi",
|
"--bin", "--bin-runtime", "--abi",
|
||||||
"--via-ir", "--optimize",
|
"--via-ir", "--optimize",
|
||||||
"--no-color",
|
"--no-color",
|
||||||
];
|
];
|
||||||
|
|
1
networks/ethereum/schnorr/.gitignore
vendored
Normal file
1
networks/ethereum/schnorr/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
artifacts
|
42
networks/ethereum/schnorr/Cargo.toml
Normal file
42
networks/ethereum/schnorr/Cargo.toml
Normal file
|
@ -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 <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
|
||||||
|
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"] }
|
15
networks/ethereum/schnorr/LICENSE
Normal file
15
networks/ethereum/schnorr/LICENSE
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
5
networks/ethereum/schnorr/README.md
Normal file
5
networks/ethereum/schnorr/README.md
Normal file
|
@ -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.
|
3
networks/ethereum/schnorr/build.rs
Normal file
3
networks/ethereum/schnorr/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
build_solidity_contracts::build("contracts", "artifacts").unwrap();
|
||||||
|
}
|
|
@ -1,24 +1,20 @@
|
||||||
// SPDX-License-Identifier: AGPLv3
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
pragma solidity ^0.8.0;
|
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 {
|
library Schnorr {
|
||||||
// secp256k1 group order
|
// secp256k1 group order
|
||||||
uint256 constant public Q =
|
uint256 constant private Q =
|
||||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
||||||
|
|
||||||
// Fixed parity for the public keys used in this contract
|
// We fix the key to have an even y coordinate to save a word when verifying
|
||||||
// This avoids spending a word passing the parity in a similar style to
|
// signatures. This is comparable to Bitcoin Taproot's encoding of keys
|
||||||
// Bitcoin's Taproot
|
uint8 constant private KEY_PARITY = 27;
|
||||||
uint8 constant public KEY_PARITY = 27;
|
|
||||||
|
|
||||||
error InvalidSOrA();
|
// px := public key x-coordinate, where the public key has an even y-coordinate
|
||||||
error MalformedSignature();
|
// message := the message signed
|
||||||
|
// c := Schnorr signature challenge
|
||||||
// px := public key x-coord, where the public key has a parity of KEY_PARITY
|
// s := Schnorr signature solution
|
||||||
// message := 32-byte hash of the message
|
|
||||||
// c := schnorr signature challenge
|
|
||||||
// s := schnorr signature
|
|
||||||
function verify(
|
function verify(
|
||||||
bytes32 px,
|
bytes32 px,
|
||||||
bytes memory message,
|
bytes memory message,
|
||||||
|
@ -31,12 +27,12 @@ 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 be 0 (sa, px, ca)
|
// For safety, we want each input to ecrecover to not be 0 (sa, px, ca)
|
||||||
// The ecreover precomple 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
|
// 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);
|
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
|
// Check the signature is correct by rebuilding the challenge
|
||||||
return c == keccak256(abi.encodePacked(R, px, message));
|
return c == keccak256(abi.encodePacked(R, px, message));
|
|
@ -1,15 +1,15 @@
|
||||||
// SPDX-License-Identifier: AGPLv3
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
pragma solidity ^0.8.0;
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
import "../../../contracts/Schnorr.sol";
|
import "../Schnorr.sol";
|
||||||
|
|
||||||
contract TestSchnorr {
|
contract TestSchnorr {
|
||||||
function verify(
|
function verify(
|
||||||
bytes32 px,
|
bytes32 public_key,
|
||||||
bytes calldata message,
|
bytes calldata message,
|
||||||
bytes32 c,
|
bytes32 c,
|
||||||
bytes32 s
|
bytes32 s
|
||||||
) external pure returns (bool) {
|
) external pure returns (bool) {
|
||||||
return Schnorr.verify(px, message, c, s);
|
return Schnorr.verify(public_key, message, c, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
2
processor/ethereum/contracts/.gitignore
vendored
2
processor/ethereum/contracts/.gitignore
vendored
|
@ -1,3 +1 @@
|
||||||
# Solidity build outputs
|
|
||||||
cache
|
|
||||||
artifacts
|
artifacts
|
||||||
|
|
|
@ -62,56 +62,6 @@ pub fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<PublicKey> {
|
|
||||||
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 = <Scalar as Reduce<KU256>>::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<Self> {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let A = Option::<AffinePoint>::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.
|
/// The HRAm to use for the Schnorr contract.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct EthereumHram {}
|
pub struct EthereumHram {}
|
||||||
|
@ -128,58 +78,6 @@ impl Hram<Secp256k1> 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<Secp256k1>,
|
|
||||||
) -> Option<Signature> {
|
|
||||||
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<Self> {
|
|
||||||
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 {
|
impl From<&Signature> for AbiSignature {
|
||||||
fn from(sig: &Signature) -> AbiSignature {
|
fn from(sig: &Signature) -> AbiSignature {
|
||||||
let c: [u8; 32] = sig.c.to_repr().into();
|
let c: [u8; 32] = sig.c.to_repr().into();
|
||||||
|
|
|
@ -23,8 +23,6 @@ mod crypto;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use contracts::tests as abi;
|
use contracts::tests as abi;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod schnorr;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod router;
|
mod router;
|
||||||
|
|
||||||
pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey) {
|
pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey) {
|
||||||
|
|
|
@ -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<RootProvider<SimpleRequest>>, 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<SimpleRequest>,
|
|
||||||
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::<Secp256k1, EthereumHram>::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());
|
|
||||||
}
|
|
Loading…
Reference in a new issue