mirror of
https://github.com/serai-dex/serai.git
synced 2024-11-16 17:07:35 +00:00
ethereum: implement schnorr verification contract deployment and related crypto (#36)
* basic schnorr verify working * add schnorr-verify as submodule * remove previous code * Misc Ethereum work which will probably be disregarded * add ecrecover hack test, worksgit add src/ * merge w develop * starting w/ rust-web3 * trying to use ethers * deploy_schnorr_verifier_contract finally working * modify EthereumHram to use 27/28 for point parity * updated address calc, solidity schnorr verify now working * add verify failure to test * update readme * move ethereum/ to coins/ * un fmt coins/monero * update .gitmodules * fix cargo paths * fix coins/monero * add #[allow(non_snake_case)] * un-fmt stuff * move crypto to coins/ethereum * move unit tests to ethereum/tests * remove js, build w ethers * update .gitignore * address comments * add q != 0 check * update contract param order * update contract license to AGPL * update ethereum-serai license to GPL and fmt * GPLv3 for ethereum-serai * AGPLv3 for ethereum-serai * actually fix license Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
parent
e67033a207
commit
c589743e2b
13 changed files with 408 additions and 2 deletions
|
@ -8,6 +8,7 @@ members = [
|
|||
"crypto/dleq",
|
||||
"crypto/frost",
|
||||
|
||||
"coins/ethereum",
|
||||
"coins/monero",
|
||||
|
||||
"processor",
|
||||
|
|
3
coins/ethereum/.gitignore
vendored
Normal file
3
coins/ethereum/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# solidity build outputs
|
||||
cache
|
||||
artifacts
|
28
coins/ethereum/Cargo.toml
Normal file
28
coins/ethereum/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "ethereum-serai"
|
||||
version = "0.1.0"
|
||||
description = "An Ethereum library supporting Schnorr signing and on-chain verification"
|
||||
license = "AGPL-3.0-only"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
serde_json = "1.0"
|
||||
serde = "1.0"
|
||||
hex-literal = "0.3"
|
||||
|
||||
ethers = { git = "https://github.com/gakonst/ethers-rs", features = ["abigen", "ethers-solc"] }
|
||||
eyre = "0.6"
|
||||
|
||||
k256 = { version = "0.11", features = ["arithmetic", "keccak256", "ecdsa"] }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["secp256k1"] }
|
||||
sha3 = "0.10"
|
||||
group = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
tokio = { version = "1.19", features = ["macros"] }
|
||||
|
||||
[build-dependencies]
|
||||
ethers-solc = { git = "https://github.com/gakonst/ethers-rs" }
|
21
coins/ethereum/README.md
Normal file
21
coins/ethereum/README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Ethereum
|
||||
|
||||
This package contains Ethereum-related functionality, specifically deploying and interacting with Serai contracts.
|
||||
|
||||
## Requirements
|
||||
|
||||
- anvil & solc & geth's abigen (see [here](https://github.com/gakonst/ethers-rs#running-the-tests))
|
||||
|
||||
## To test
|
||||
|
||||
To compile contracts:
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
This places the compiled artifact into `artifacts/`.
|
||||
|
||||
To run Rust tests (you must have compiled the contracts first):
|
||||
```
|
||||
cargo test
|
||||
```
|
15
coins/ethereum/build.rs
Normal file
15
coins/ethereum/build.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use ethers_solc::{Project, ProjectPathsConfig};
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=contracts/Schnorr.sol");
|
||||
|
||||
// configure the project with all its paths, solc, cache etc.
|
||||
let project = Project::builder()
|
||||
.paths(ProjectPathsConfig::hardhat(env!("CARGO_MANIFEST_DIR")).unwrap())
|
||||
.build()
|
||||
.unwrap();
|
||||
project.compile().unwrap();
|
||||
|
||||
// Tell Cargo that if a source file changes, to rerun this build script.
|
||||
project.rerun_if_sources_changed();
|
||||
}
|
36
coins/ethereum/contracts/Schnorr.sol
Normal file
36
coins/ethereum/contracts/Schnorr.sol
Normal file
|
@ -0,0 +1,36 @@
|
|||
//SPDX-License-Identifier: AGPLv3
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
// see https://github.com/noot/schnorr-verify for implementation details
|
||||
contract Schnorr {
|
||||
// secp256k1 group order
|
||||
uint256 constant public Q =
|
||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
||||
|
||||
// parity := public key y-coord parity (27 or 28)
|
||||
// px := public key x-coord
|
||||
// message := 32-byte message
|
||||
// s := schnorr signature
|
||||
// e := schnorr signature challenge
|
||||
function verify(
|
||||
uint8 parity,
|
||||
bytes32 px,
|
||||
bytes32 message,
|
||||
bytes32 s,
|
||||
bytes32 e
|
||||
) public view returns (bool) {
|
||||
// ecrecover = (m, v, r, s);
|
||||
bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
||||
bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
|
||||
|
||||
require(sp != 0);
|
||||
// the ecrecover precompile implementation checks that the `r` and `s`
|
||||
// inputs are non-zero (in this case, `px` and `ep`), thus we don't need to
|
||||
// check if they're zero.will make me
|
||||
address R = ecrecover(sp, parity, px, ep);
|
||||
require(R != address(0), "ecrecover failed");
|
||||
return e == keccak256(
|
||||
abi.encodePacked(R, uint8(parity), px, block.chainid, message)
|
||||
);
|
||||
}
|
||||
}
|
52
coins/ethereum/src/contract.rs
Normal file
52
coins/ethereum/src/contract.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::crypto::ProcessedSignature;
|
||||
use ethers::{contract::ContractFactory, prelude::*, solc::artifacts::contract::ContractBytecode};
|
||||
use eyre::{eyre, Result};
|
||||
use std::fs::File;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EthereumError {
|
||||
#[error("failed to verify Schnorr signature")]
|
||||
VerificationError,
|
||||
}
|
||||
|
||||
abigen!(
|
||||
Schnorr,
|
||||
"./artifacts/Schnorr.sol/Schnorr.json",
|
||||
event_derives(serde::Deserialize, serde::Serialize),
|
||||
);
|
||||
|
||||
pub async fn deploy_schnorr_verifier_contract(
|
||||
client: Arc<SignerMiddleware<Provider<Http>, LocalWallet>>,
|
||||
) -> Result<schnorr_mod::Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>> {
|
||||
let path = "./artifacts/Schnorr.sol/Schnorr.json";
|
||||
let artifact: ContractBytecode = serde_json::from_reader(File::open(path).unwrap()).unwrap();
|
||||
let abi = artifact.abi.unwrap();
|
||||
let bin = artifact.bytecode.unwrap().object;
|
||||
let factory = ContractFactory::new(abi, bin.into_bytes().unwrap(), client.clone());
|
||||
let contract = factory.deploy(())?.send().await?;
|
||||
let contract = Schnorr::new(contract.address(), client);
|
||||
Ok(contract)
|
||||
}
|
||||
|
||||
pub async fn call_verify(
|
||||
contract: &schnorr_mod::Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>,
|
||||
params: &ProcessedSignature,
|
||||
) -> Result<()> {
|
||||
let ok = contract
|
||||
.verify(
|
||||
params.parity + 27,
|
||||
params.px.to_bytes().into(),
|
||||
params.message.into(),
|
||||
params.s.to_bytes().into(),
|
||||
params.e.to_bytes().into(),
|
||||
)
|
||||
.call()
|
||||
.await?;
|
||||
if ok {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(eyre!(EthereumError::VerificationError))
|
||||
}
|
||||
}
|
104
coins/ethereum/src/crypto.rs
Normal file
104
coins/ethereum/src/crypto.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use sha3::{Digest, Keccak256};
|
||||
|
||||
use group::Group;
|
||||
use k256::{
|
||||
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, sec1::ToEncodedPoint, DecompressPoint},
|
||||
AffinePoint, ProjectivePoint, Scalar, U256,
|
||||
};
|
||||
|
||||
use frost::{algorithm::Hram, curve::Secp256k1};
|
||||
|
||||
pub fn keccak256(data: &[u8]) -> [u8; 32] {
|
||||
Keccak256::digest(data).try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||
Scalar::from_uint_reduced(U256::from_be_slice(&keccak256(data)))
|
||||
}
|
||||
|
||||
pub fn address(point: &ProjectivePoint) -> [u8; 20] {
|
||||
let encoded_point = point.to_encoded_point(false);
|
||||
keccak256(&encoded_point.as_ref()[1 .. 65])[12 .. 32].try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn ecrecover(message: Scalar, v: u8, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
||||
if r.is_zero().into() || s.is_zero().into() {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let R = AffinePoint::decompress(&r.to_bytes(), v.into());
|
||||
#[allow(non_snake_case)]
|
||||
if let Some(R) = Option::<AffinePoint>::from(R) {
|
||||
#[allow(non_snake_case)]
|
||||
let R = ProjectivePoint::from(R);
|
||||
|
||||
let r = r.invert().unwrap();
|
||||
let u1 = ProjectivePoint::GENERATOR * (-message * r);
|
||||
let u2 = R * (s * r);
|
||||
let key: ProjectivePoint = u1 + u2;
|
||||
if !bool::from(key.is_identity()) {
|
||||
return Some(address(&key));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct EthereumHram {}
|
||||
impl Hram<Secp256k1> for EthereumHram {
|
||||
#[allow(non_snake_case)]
|
||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
||||
let a_encoded_point = A.to_encoded_point(true);
|
||||
let mut a_encoded = a_encoded_point.as_ref().to_owned();
|
||||
a_encoded[0] += 25; // Ethereum uses 27/28 for point parity
|
||||
let mut data = address(R).to_vec();
|
||||
data.append(&mut a_encoded);
|
||||
data.append(&mut m.to_vec());
|
||||
Scalar::from_uint_reduced(U256::from_be_slice(&keccak256(&data)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcessedSignature {
|
||||
pub s: Scalar,
|
||||
pub px: Scalar,
|
||||
pub parity: u8,
|
||||
pub message: [u8; 32],
|
||||
pub e: Scalar,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn preprocess_signature_for_ecrecover(
|
||||
m: [u8; 32],
|
||||
R: &ProjectivePoint,
|
||||
s: Scalar,
|
||||
A: &ProjectivePoint,
|
||||
chain_id: U256,
|
||||
) -> (Scalar, Scalar) {
|
||||
let processed_sig = process_signature_for_contract(m, R, s, A, chain_id);
|
||||
let sr = processed_sig.s.mul(&processed_sig.px).negate();
|
||||
let er = processed_sig.e.mul(&processed_sig.px).negate();
|
||||
(sr, er)
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn process_signature_for_contract(
|
||||
m: [u8; 32],
|
||||
R: &ProjectivePoint,
|
||||
s: Scalar,
|
||||
A: &ProjectivePoint,
|
||||
chain_id: U256,
|
||||
) -> ProcessedSignature {
|
||||
let encoded_pk = A.to_encoded_point(true);
|
||||
let px = &encoded_pk.as_ref()[1 .. 33];
|
||||
let px_scalar = Scalar::from_uint_reduced(U256::from_be_slice(px));
|
||||
let e = EthereumHram::hram(R, A, &[chain_id.to_be_byte_array().as_slice(), &m].concat());
|
||||
ProcessedSignature {
|
||||
s,
|
||||
px: px_scalar,
|
||||
parity: &encoded_pk.as_ref()[0] - 2,
|
||||
#[allow(non_snake_case)]
|
||||
message: m,
|
||||
e,
|
||||
}
|
||||
}
|
2
coins/ethereum/src/lib.rs
Normal file
2
coins/ethereum/src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod contract;
|
||||
pub mod crypto;
|
60
coins/ethereum/tests/contract.rs
Normal file
60
coins/ethereum/tests/contract.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use ethereum_serai::contract::{call_verify, deploy_schnorr_verifier_contract};
|
||||
use ethers::{prelude::*, utils::Anvil};
|
||||
use std::{convert::TryFrom, sync::Arc, time::Duration};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deploy_contract() {
|
||||
let anvil = Anvil::new().spawn();
|
||||
let wallet: LocalWallet = anvil.keys()[0].clone().into();
|
||||
let provider =
|
||||
Provider::<Http>::try_from(anvil.endpoint()).unwrap().interval(Duration::from_millis(10u64));
|
||||
let client = Arc::new(SignerMiddleware::new(provider, wallet));
|
||||
|
||||
let _contract = deploy_schnorr_verifier_contract(client).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ecrecover_hack() {
|
||||
use ethereum_serai::crypto;
|
||||
use ethers::utils::keccak256;
|
||||
use frost::{
|
||||
algorithm::Schnorr,
|
||||
curve::Secp256k1,
|
||||
tests::{algorithm_machines, key_gen, sign},
|
||||
};
|
||||
use k256::elliptic_curve::bigint::ArrayEncoding;
|
||||
use k256::{Scalar, U256};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let anvil = Anvil::new().spawn();
|
||||
let wallet: LocalWallet = anvil.keys()[0].clone().into();
|
||||
let provider =
|
||||
Provider::<Http>::try_from(anvil.endpoint()).unwrap().interval(Duration::from_millis(10u64));
|
||||
let chain_id = provider.get_chainid().await.unwrap();
|
||||
let client = Arc::new(SignerMiddleware::new(provider, wallet));
|
||||
|
||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||
let group_key = keys[&1].group_key();
|
||||
|
||||
const MESSAGE: &'static [u8] = b"Hello, World!";
|
||||
let hashed_message = keccak256(MESSAGE);
|
||||
let chain_id = U256::from(Scalar::from(chain_id.as_u32()));
|
||||
|
||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||
|
||||
let sig = sign(
|
||||
&mut OsRng,
|
||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, crypto::EthereumHram>::new(), &keys),
|
||||
full_message,
|
||||
);
|
||||
let mut processed_sig =
|
||||
crypto::process_signature_for_contract(hashed_message, &sig.R, sig.s, &group_key, chain_id);
|
||||
|
||||
let contract = deploy_schnorr_verifier_contract(client).await.unwrap();
|
||||
call_verify(&contract, &processed_sig).await.unwrap();
|
||||
|
||||
// test invalid signature fails
|
||||
processed_sig.message[0] = 0;
|
||||
let res = call_verify(&contract, &processed_sig).await;
|
||||
assert!(res.is_err());
|
||||
}
|
80
coins/ethereum/tests/crypto.rs
Normal file
80
coins/ethereum/tests/crypto.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use ethereum_serai::crypto::*;
|
||||
use frost::curve::Secp256k1;
|
||||
use k256::{
|
||||
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, sec1::ToEncodedPoint},
|
||||
ProjectivePoint, Scalar, U256,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_ecrecover() {
|
||||
use k256::ecdsa::{
|
||||
recoverable::Signature,
|
||||
signature::{Signer, Verifier},
|
||||
SigningKey, VerifyingKey,
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let private = SigningKey::random(&mut OsRng);
|
||||
let public = VerifyingKey::from(&private);
|
||||
|
||||
const MESSAGE: &'static [u8] = b"Hello, World!";
|
||||
let sig: Signature = private.sign(MESSAGE);
|
||||
public.verify(MESSAGE, &sig).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
ecrecover(hash_to_scalar(MESSAGE), sig.as_ref()[64], *sig.r(), *sig.s()).unwrap(),
|
||||
address(&ProjectivePoint::from(public))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signing() {
|
||||
use frost::{
|
||||
algorithm::Schnorr,
|
||||
tests::{algorithm_machines, key_gen, sign},
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||
let _group_key = keys[&1].group_key();
|
||||
|
||||
const MESSAGE: &'static [u8] = b"Hello, World!";
|
||||
|
||||
let _sig = sign(
|
||||
&mut OsRng,
|
||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
||||
MESSAGE,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecrecover_hack() {
|
||||
use frost::{
|
||||
algorithm::Schnorr,
|
||||
tests::{algorithm_machines, key_gen, sign},
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||
let group_key = keys[&1].group_key();
|
||||
let group_key_encoded = group_key.to_encoded_point(true);
|
||||
let group_key_compressed = group_key_encoded.as_ref();
|
||||
let group_key_x = Scalar::from_uint_reduced(U256::from_be_slice(&group_key_compressed[1 .. 33]));
|
||||
|
||||
const MESSAGE: &'static [u8] = b"Hello, World!";
|
||||
let hashed_message = keccak256(MESSAGE);
|
||||
let chain_id = U256::from(Scalar::ONE);
|
||||
|
||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||
|
||||
let sig = sign(
|
||||
&mut OsRng,
|
||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
||||
full_message,
|
||||
);
|
||||
|
||||
let (sr, er) =
|
||||
preprocess_signature_for_ecrecover(hashed_message, &sig.R, sig.s, &group_key, chain_id);
|
||||
let q = ecrecover(sr, group_key_compressed[0] - 2, group_key_x, er).unwrap();
|
||||
assert_eq!(q, address(&sig.R));
|
||||
}
|
2
coins/ethereum/tests/mod.rs
Normal file
2
coins/ethereum/tests/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod contract;
|
||||
mod crypto;
|
|
@ -18,14 +18,16 @@ hex = "0.4"
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
curve25519-dalek = { version = "3", features = ["std"] }
|
||||
sha3 = "0.10"
|
||||
blake2 = "0.10"
|
||||
|
||||
group = "0.12"
|
||||
k256 = { version = "0.11", features = ["arithmetic", "keccak256", "ecdsa"] }
|
||||
curve25519-dalek = { version = "3", features = ["std"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../crypto/transcript", features = ["recommended"] }
|
||||
dalek-ff-group = { path = "../crypto/dalek-ff-group" }
|
||||
frost = { package = "modular-frost", path = "../crypto/frost" }
|
||||
frost = { package = "modular-frost", path = "../crypto/frost", features = ["secp256k1", "ed25519"] }
|
||||
|
||||
monero = { version = "0.16", features = ["experimental"] }
|
||||
monero-serai = { path = "../coins/monero", features = ["multisig"] }
|
||||
|
|
Loading…
Reference in a new issue