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:
noot 2022-07-16 21:45:41 +00:00 committed by GitHub
parent e67033a207
commit c589743e2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 408 additions and 2 deletions

View file

@ -8,6 +8,7 @@ members = [
"crypto/dleq",
"crypto/frost",
"coins/ethereum",
"coins/monero",
"processor",

3
coins/ethereum/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# solidity build outputs
cache
artifacts

28
coins/ethereum/Cargo.toml Normal file
View 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
View 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
View 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();
}

View 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)
);
}
}

View 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))
}
}

View 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,
}
}

View file

@ -0,0 +1,2 @@
pub mod contract;
pub mod crypto;

View 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());
}

View 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));
}

View file

@ -0,0 +1,2 @@
mod contract;
mod crypto;

View file

@ -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"] }