mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 11:39:35 +00:00
implement Router.sol and associated functions (#92)
* start Router contract * use calldata for function args * var name changes * start testing router contract * test with and without abi.encode * cleanup * why tf isn't tests/utils working * cleanup tests * remove unused files * wip * fix router contract and tests, add set/update public keys funcs * impl some Froms * make execute non-reentrant * cleanup * update Router to use ReentrancyGuard * update contract to use errors, use bitfield in Executed event, minor other fixes * wip * fix build issues from merge, tests ok * Router.sol cleanup * cleanup, uncomment stuff * bump ethers.rs version to latest * make contract functions take generic middleware * update build script to assert no compiler errors * hardcode pubkey parity into contract, update tests * Polish coins/ethereum in various ways --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
parent
3d855c75be
commit
63521f6a96
20 changed files with 690 additions and 348 deletions
|
@ -42,8 +42,8 @@ runs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cargo install svm-rs
|
cargo install svm-rs
|
||||||
svm install 0.8.16
|
svm install 0.8.25
|
||||||
svm use 0.8.16
|
svm use 0.8.25
|
||||||
|
|
||||||
# - name: Cache Rust
|
# - name: Cache Rust
|
||||||
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43
|
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43
|
||||||
|
|
6
coins/ethereum/.gitignore
vendored
6
coins/ethereum/.gitignore
vendored
|
@ -1,3 +1,7 @@
|
||||||
# solidity build outputs
|
# Solidity build outputs
|
||||||
cache
|
cache
|
||||||
artifacts
|
artifacts
|
||||||
|
|
||||||
|
# Auto-generated ABI files
|
||||||
|
src/abi/schnorr.rs
|
||||||
|
src/abi/router.rs
|
||||||
|
|
|
@ -30,6 +30,9 @@ ethers-core = { version = "2", default-features = false }
|
||||||
ethers-providers = { version = "2", default-features = false }
|
ethers-providers = { version = "2", default-features = false }
|
||||||
ethers-contract = { version = "2", default-features = false, features = ["abigen", "providers"] }
|
ethers-contract = { version = "2", default-features = false, features = ["abigen", "providers"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
ethers-contract = { version = "2", default-features = false, features = ["abigen", "providers"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,20 @@
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use ethers_contract::Abigen;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-changed=contracts");
|
println!("cargo:rerun-if-changed=contracts/*");
|
||||||
println!("cargo:rerun-if-changed=artifacts");
|
println!("cargo:rerun-if-changed=artifacts/*");
|
||||||
|
|
||||||
|
for line in String::from_utf8(Command::new("solc").args(["--version"]).output().unwrap().stdout)
|
||||||
|
.unwrap()
|
||||||
|
.lines()
|
||||||
|
{
|
||||||
|
if let Some(version) = line.strip_prefix("Version: ") {
|
||||||
|
let version = version.split('+').next().unwrap();
|
||||||
|
assert_eq!(version, "0.8.25");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
let args = [
|
let args = [
|
||||||
|
@ -8,8 +22,21 @@ fn main() {
|
||||||
"-o", "./artifacts", "--overwrite",
|
"-o", "./artifacts", "--overwrite",
|
||||||
"--bin", "--abi",
|
"--bin", "--abi",
|
||||||
"--optimize",
|
"--optimize",
|
||||||
"./contracts/Schnorr.sol"
|
"./contracts/Schnorr.sol", "./contracts/Router.sol",
|
||||||
];
|
];
|
||||||
|
assert!(Command::new("solc").args(args).status().unwrap().success());
|
||||||
|
|
||||||
assert!(std::process::Command::new("solc").args(args).status().unwrap().success());
|
Abigen::new("Schnorr", "./artifacts/Schnorr.abi")
|
||||||
|
.unwrap()
|
||||||
|
.generate()
|
||||||
|
.unwrap()
|
||||||
|
.write_to_file("./src/abi/schnorr.rs")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Abigen::new("Router", "./artifacts/Router.abi")
|
||||||
|
.unwrap()
|
||||||
|
.generate()
|
||||||
|
.unwrap()
|
||||||
|
.write_to_file("./src/abi/router.rs")
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
90
coins/ethereum/contracts/Router.sol
Normal file
90
coins/ethereum/contracts/Router.sol
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// SPDX-License-Identifier: AGPLv3
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "./Schnorr.sol";
|
||||||
|
|
||||||
|
contract Router is Schnorr {
|
||||||
|
// Contract initializer
|
||||||
|
// TODO: Replace with a MuSig of the genesis validators
|
||||||
|
address public initializer;
|
||||||
|
|
||||||
|
// Nonce is incremented for each batch of transactions executed
|
||||||
|
uint256 public nonce;
|
||||||
|
|
||||||
|
// fixed parity for the public keys used in this contract
|
||||||
|
uint8 constant public KEY_PARITY = 27;
|
||||||
|
|
||||||
|
// current public key's x-coordinate
|
||||||
|
// note: this key must always use the fixed parity defined above
|
||||||
|
bytes32 public seraiKey;
|
||||||
|
|
||||||
|
struct OutInstruction {
|
||||||
|
address to;
|
||||||
|
uint256 value;
|
||||||
|
bytes data;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Signature {
|
||||||
|
bytes32 c;
|
||||||
|
bytes32 s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// success is a uint256 representing a bitfield of transaction successes
|
||||||
|
event Executed(uint256 nonce, bytes32 batch, uint256 success);
|
||||||
|
|
||||||
|
// error types
|
||||||
|
error NotInitializer();
|
||||||
|
error AlreadyInitialized();
|
||||||
|
error InvalidKey();
|
||||||
|
error TooManyTransactions();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
initializer = msg.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSeraiKey can be called by the contract initializer to set the first
|
||||||
|
// public key, only if the public key has yet to be set.
|
||||||
|
function initSeraiKey(bytes32 _seraiKey) external {
|
||||||
|
if (msg.sender != initializer) revert NotInitializer();
|
||||||
|
if (seraiKey != 0) revert AlreadyInitialized();
|
||||||
|
if (_seraiKey == bytes32(0)) revert InvalidKey();
|
||||||
|
seraiKey = _seraiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSeraiKey validates the given Schnorr signature against the current public key,
|
||||||
|
// and if successful, updates the contract's public key to the given one.
|
||||||
|
function updateSeraiKey(
|
||||||
|
bytes32 _seraiKey,
|
||||||
|
Signature memory sig
|
||||||
|
) public {
|
||||||
|
if (_seraiKey == bytes32(0)) revert InvalidKey();
|
||||||
|
bytes32 message = keccak256(abi.encodePacked("updateSeraiKey", _seraiKey));
|
||||||
|
if (!verify(KEY_PARITY, seraiKey, message, sig.c, sig.s)) revert InvalidSignature();
|
||||||
|
seraiKey = _seraiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute accepts a list of transactions to execute as well as a Schnorr signature.
|
||||||
|
// if signature verification passes, the given transactions are executed.
|
||||||
|
// if signature verification fails, this function will revert.
|
||||||
|
function execute(
|
||||||
|
OutInstruction[] calldata transactions,
|
||||||
|
Signature memory sig
|
||||||
|
) public {
|
||||||
|
if (transactions.length > 256) revert TooManyTransactions();
|
||||||
|
|
||||||
|
bytes32 message = keccak256(abi.encode("execute", nonce, transactions));
|
||||||
|
// This prevents re-entrancy from causing double spends yet does allow
|
||||||
|
// out-of-order execution via re-entrancy
|
||||||
|
nonce++;
|
||||||
|
if (!verify(KEY_PARITY, seraiKey, message, sig.c, sig.s)) revert InvalidSignature();
|
||||||
|
|
||||||
|
uint256 successes;
|
||||||
|
for(uint256 i = 0; i < transactions.length; i++) {
|
||||||
|
(bool success, ) = transactions[i].to.call{value: transactions[i].value, gas: 200_000}(transactions[i].data);
|
||||||
|
assembly {
|
||||||
|
successes := or(successes, shl(i, success))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit Executed(nonce, message, successes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
//SPDX-License-Identifier: AGPLv3
|
// SPDX-License-Identifier: AGPLv3
|
||||||
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
|
||||||
|
@ -7,29 +7,32 @@ contract Schnorr {
|
||||||
uint256 constant public Q =
|
uint256 constant public Q =
|
||||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
||||||
|
|
||||||
|
error InvalidSOrA();
|
||||||
|
error InvalidSignature();
|
||||||
|
|
||||||
// parity := public key y-coord parity (27 or 28)
|
// parity := public key y-coord parity (27 or 28)
|
||||||
// px := public key x-coord
|
// px := public key x-coord
|
||||||
// message := 32-byte message
|
// message := 32-byte hash of the message
|
||||||
|
// c := schnorr signature challenge
|
||||||
// s := schnorr signature
|
// s := schnorr signature
|
||||||
// e := schnorr signature challenge
|
|
||||||
function verify(
|
function verify(
|
||||||
uint8 parity,
|
uint8 parity,
|
||||||
bytes32 px,
|
bytes32 px,
|
||||||
bytes32 message,
|
bytes32 message,
|
||||||
bytes32 s,
|
bytes32 c,
|
||||||
bytes32 e
|
bytes32 s
|
||||||
) public view returns (bool) {
|
) public view returns (bool) {
|
||||||
// ecrecover = (m, v, r, s);
|
// ecrecover = (m, v, r, s);
|
||||||
bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
bytes32 sa = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
||||||
bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
|
bytes32 ca = bytes32(Q - mulmod(uint256(c), uint256(px), Q));
|
||||||
|
|
||||||
require(sp != 0);
|
if (sa == 0) revert InvalidSOrA();
|
||||||
// the ecrecover precompile implementation checks that the `r` and `s`
|
// 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
|
// inputs are non-zero (in this case, `px` and `ca`), thus we don't need to
|
||||||
// check if they're zero.will make me
|
// check if they're zero.
|
||||||
address R = ecrecover(sp, parity, px, ep);
|
address R = ecrecover(sa, parity, px, ca);
|
||||||
require(R != address(0), "ecrecover failed");
|
if (R == address(0)) revert InvalidSignature();
|
||||||
return e == keccak256(
|
return c == keccak256(
|
||||||
abi.encodePacked(R, uint8(parity), px, block.chainid, message)
|
abi.encodePacked(R, uint8(parity), px, block.chainid, message)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
6
coins/ethereum/src/abi/mod.rs
Normal file
6
coins/ethereum/src/abi/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[allow(clippy::all)]
|
||||||
|
pub(crate) mod schnorr;
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[allow(clippy::all)]
|
||||||
|
pub(crate) mod router;
|
|
@ -1,36 +0,0 @@
|
||||||
use thiserror::Error;
|
|
||||||
use eyre::{eyre, Result};
|
|
||||||
|
|
||||||
use ethers_providers::{Provider, Http};
|
|
||||||
use ethers_contract::abigen;
|
|
||||||
|
|
||||||
use crate::crypto::ProcessedSignature;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum EthereumError {
|
|
||||||
#[error("failed to verify Schnorr signature")]
|
|
||||||
VerificationError,
|
|
||||||
}
|
|
||||||
|
|
||||||
abigen!(Schnorr, "./artifacts/Schnorr.abi");
|
|
||||||
|
|
||||||
pub async fn call_verify(
|
|
||||||
contract: &Schnorr<Provider<Http>>,
|
|
||||||
params: &ProcessedSignature,
|
|
||||||
) -> Result<()> {
|
|
||||||
if contract
|
|
||||||
.verify(
|
|
||||||
params.parity + 27,
|
|
||||||
params.px.to_bytes().into(),
|
|
||||||
params.message,
|
|
||||||
params.s.to_bytes().into(),
|
|
||||||
params.e.to_bytes().into(),
|
|
||||||
)
|
|
||||||
.call()
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(eyre!(EthereumError::VerificationError))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +1,54 @@
|
||||||
use sha3::{Digest, Keccak256};
|
use sha3::{Digest, Keccak256};
|
||||||
|
|
||||||
use group::Group;
|
use group::ff::PrimeField;
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::{
|
elliptic_curve::{
|
||||||
bigint::ArrayEncoding, ops::Reduce, point::DecompressPoint, sec1::ToEncodedPoint,
|
bigint::ArrayEncoding, ops::Reduce, point::AffineCoordinates, sec1::ToEncodedPoint,
|
||||||
},
|
},
|
||||||
AffinePoint, ProjectivePoint, Scalar, U256,
|
ProjectivePoint, Scalar, U256,
|
||||||
};
|
};
|
||||||
|
|
||||||
use frost::{algorithm::Hram, curve::Secp256k1};
|
use frost::{
|
||||||
|
algorithm::{Hram, SchnorrSignature},
|
||||||
|
curve::Secp256k1,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn keccak256(data: &[u8]) -> [u8; 32] {
|
pub(crate) fn keccak256(data: &[u8]) -> [u8; 32] {
|
||||||
Keccak256::digest(data).into()
|
Keccak256::digest(data).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
pub(crate) fn address(point: &ProjectivePoint) -> [u8; 20] {
|
||||||
Scalar::reduce(U256::from_be_slice(&keccak256(data)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(point: &ProjectivePoint) -> [u8; 20] {
|
|
||||||
let encoded_point = point.to_encoded_point(false);
|
let encoded_point = point.to_encoded_point(false);
|
||||||
keccak256(&encoded_point.as_ref()[1 .. 65])[12 .. 32].try_into().unwrap()
|
// Last 20 bytes of the hash of the concatenated x and y coordinates
|
||||||
|
// We obtain the concatenated x and y coordinates via the uncompressed encoding of the point
|
||||||
|
keccak256(&encoded_point.as_ref()[1 .. 65])[12 ..].try_into().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ecrecover(message: Scalar, v: u8, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
#[allow(non_snake_case)]
|
||||||
if r.is_zero().into() || s.is_zero().into() {
|
pub struct PublicKey {
|
||||||
return None;
|
pub A: ProjectivePoint,
|
||||||
}
|
pub px: Scalar,
|
||||||
|
pub parity: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PublicKey {
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
let R = AffinePoint::decompress(&r.to_bytes(), v.into());
|
pub fn new(A: ProjectivePoint) -> Option<PublicKey> {
|
||||||
#[allow(non_snake_case)]
|
let affine = A.to_affine();
|
||||||
if let Some(R) = Option::<AffinePoint>::from(R) {
|
let parity = u8::from(bool::from(affine.y_is_odd())) + 27;
|
||||||
#[allow(non_snake_case)]
|
if parity != 27 {
|
||||||
let R = ProjectivePoint::from(R);
|
None?;
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
None
|
let x_coord = affine.x();
|
||||||
|
let x_coord_scalar = <Scalar as Reduce<U256>>::reduce_bytes(&x_coord);
|
||||||
|
// Return None if a reduction would occur
|
||||||
|
if x_coord_scalar.to_repr() != x_coord {
|
||||||
|
None?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(PublicKey { A, px: x_coord_scalar, parity })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
@ -55,53 +59,33 @@ impl Hram<Secp256k1> for EthereumHram {
|
||||||
let a_encoded_point = A.to_encoded_point(true);
|
let a_encoded_point = A.to_encoded_point(true);
|
||||||
let mut a_encoded = a_encoded_point.as_ref().to_owned();
|
let mut a_encoded = a_encoded_point.as_ref().to_owned();
|
||||||
a_encoded[0] += 25; // Ethereum uses 27/28 for point parity
|
a_encoded[0] += 25; // Ethereum uses 27/28 for point parity
|
||||||
|
assert!((a_encoded[0] == 27) || (a_encoded[0] == 28));
|
||||||
let mut data = address(R).to_vec();
|
let mut data = address(R).to_vec();
|
||||||
data.append(&mut a_encoded);
|
data.append(&mut a_encoded);
|
||||||
data.append(&mut m.to_vec());
|
data.extend(m);
|
||||||
Scalar::reduce(U256::from_be_slice(&keccak256(&data)))
|
Scalar::reduce(U256::from_be_slice(&keccak256(&data)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProcessedSignature {
|
pub struct Signature {
|
||||||
pub s: Scalar,
|
pub(crate) c: Scalar,
|
||||||
pub px: Scalar,
|
pub(crate) s: Scalar,
|
||||||
pub parity: u8,
|
|
||||||
pub message: [u8; 32],
|
|
||||||
pub e: Scalar,
|
|
||||||
}
|
}
|
||||||
|
impl Signature {
|
||||||
#[allow(non_snake_case)]
|
pub fn new(
|
||||||
pub fn preprocess_signature_for_ecrecover(
|
public_key: &PublicKey,
|
||||||
m: [u8; 32],
|
chain_id: U256,
|
||||||
R: &ProjectivePoint,
|
m: &[u8],
|
||||||
s: Scalar,
|
signature: SchnorrSignature<Secp256k1>,
|
||||||
A: &ProjectivePoint,
|
) -> Option<Signature> {
|
||||||
chain_id: U256,
|
let c = EthereumHram::hram(
|
||||||
) -> (Scalar, Scalar) {
|
&signature.R,
|
||||||
let processed_sig = process_signature_for_contract(m, R, s, A, chain_id);
|
&public_key.A,
|
||||||
let sr = processed_sig.s.mul(&processed_sig.px).negate();
|
&[chain_id.to_be_byte_array().as_slice(), &keccak256(m)].concat(),
|
||||||
let er = processed_sig.e.mul(&processed_sig.px).negate();
|
);
|
||||||
(sr, er)
|
if !signature.verify(public_key.A, c) {
|
||||||
}
|
None?;
|
||||||
|
}
|
||||||
#[allow(non_snake_case)]
|
Some(Signature { c, s: signature.s })
|
||||||
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::reduce(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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,16 @@
|
||||||
pub mod contract;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
|
||||||
|
pub(crate) mod abi;
|
||||||
|
pub mod schnorr;
|
||||||
|
pub mod router;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("failed to verify Schnorr signature")]
|
||||||
|
InvalidSignature,
|
||||||
|
}
|
||||||
|
|
30
coins/ethereum/src/router.rs
Normal file
30
coins/ethereum/src/router.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
pub use crate::abi::router::*;
|
||||||
|
|
||||||
|
/*
|
||||||
|
use crate::crypto::{ProcessedSignature, PublicKey};
|
||||||
|
use ethers::{contract::ContractFactory, prelude::*, solc::artifacts::contract::ContractBytecode};
|
||||||
|
use eyre::Result;
|
||||||
|
use std::{convert::From, fs::File, sync::Arc};
|
||||||
|
|
||||||
|
pub async fn router_update_public_key<M: Middleware + 'static>(
|
||||||
|
contract: &Router<M>,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
signature: &ProcessedSignature,
|
||||||
|
) -> std::result::Result<Option<TransactionReceipt>, eyre::ErrReport> {
|
||||||
|
let tx = contract.update_public_key(public_key.px.to_bytes().into(), signature.into());
|
||||||
|
let pending_tx = tx.send().await?;
|
||||||
|
let receipt = pending_tx.await?;
|
||||||
|
Ok(receipt)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn router_execute<M: Middleware + 'static>(
|
||||||
|
contract: &Router<M>,
|
||||||
|
txs: Vec<Rtransaction>,
|
||||||
|
signature: &ProcessedSignature,
|
||||||
|
) -> std::result::Result<Option<TransactionReceipt>, eyre::ErrReport> {
|
||||||
|
let tx = contract.execute(txs, signature.into()).send();
|
||||||
|
let pending_tx = tx.send().await?;
|
||||||
|
let receipt = pending_tx.await?;
|
||||||
|
Ok(receipt)
|
||||||
|
}
|
||||||
|
*/
|
34
coins/ethereum/src/schnorr.rs
Normal file
34
coins/ethereum/src/schnorr.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use eyre::{eyre, Result};
|
||||||
|
|
||||||
|
use group::ff::PrimeField;
|
||||||
|
|
||||||
|
use ethers_providers::{Provider, Http};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Error,
|
||||||
|
crypto::{keccak256, PublicKey, Signature},
|
||||||
|
};
|
||||||
|
pub use crate::abi::schnorr::*;
|
||||||
|
|
||||||
|
pub async fn call_verify(
|
||||||
|
contract: &Schnorr<Provider<Http>>,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
message: &[u8],
|
||||||
|
signature: &Signature,
|
||||||
|
) -> Result<()> {
|
||||||
|
if contract
|
||||||
|
.verify(
|
||||||
|
public_key.parity,
|
||||||
|
public_key.px.to_repr().into(),
|
||||||
|
keccak256(message),
|
||||||
|
signature.c.to_repr().into(),
|
||||||
|
signature.s.to_repr().into(),
|
||||||
|
)
|
||||||
|
.call()
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(eyre!(Error::InvalidSignature))
|
||||||
|
}
|
||||||
|
}
|
132
coins/ethereum/src/tests/crypto.rs
Normal file
132
coins/ethereum/src/tests/crypto.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use sha2::Sha256;
|
||||||
|
use sha3::{Digest, Keccak256};
|
||||||
|
|
||||||
|
use group::Group;
|
||||||
|
use k256::{
|
||||||
|
ecdsa::{hazmat::SignPrimitive, signature::DigestVerifier, SigningKey, VerifyingKey},
|
||||||
|
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, point::DecompressPoint},
|
||||||
|
U256, Scalar, AffinePoint, ProjectivePoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
use frost::{
|
||||||
|
curve::Secp256k1,
|
||||||
|
algorithm::{Hram, IetfSchnorr},
|
||||||
|
tests::{algorithm_machines, sign},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{crypto::*, tests::key_gen};
|
||||||
|
|
||||||
|
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||||
|
Scalar::reduce(U256::from_be_slice(&keccak256(data)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ecrecover(message: Scalar, v: u8, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
||||||
|
if r.is_zero().into() || s.is_zero().into() || !((v == 27) || (v == 28)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let R = AffinePoint::decompress(&r.to_bytes(), (v - 27).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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ecrecover() {
|
||||||
|
let private = SigningKey::random(&mut OsRng);
|
||||||
|
let public = VerifyingKey::from(&private);
|
||||||
|
|
||||||
|
// Sign the signature
|
||||||
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
|
let (sig, recovery_id) = private
|
||||||
|
.as_nonzero_scalar()
|
||||||
|
.try_sign_prehashed_rfc6979::<Sha256>(&Keccak256::digest(MESSAGE), b"")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Sanity check the signature verifies
|
||||||
|
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
||||||
|
{
|
||||||
|
assert_eq!(public.verify_digest(Keccak256::new_with_prefix(MESSAGE), &sig).unwrap(), ());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the ecrecover
|
||||||
|
assert_eq!(
|
||||||
|
ecrecover(
|
||||||
|
hash_to_scalar(MESSAGE),
|
||||||
|
u8::from(recovery_id.unwrap().is_y_odd()) + 27,
|
||||||
|
*sig.r(),
|
||||||
|
*sig.s()
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
address(&ProjectivePoint::from(public.as_affine()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the sign test with the EthereumHram
|
||||||
|
#[test]
|
||||||
|
fn test_signing() {
|
||||||
|
let (keys, _) = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn preprocess_signature_for_ecrecover(
|
||||||
|
R: ProjectivePoint,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
chain_id: U256,
|
||||||
|
m: &[u8],
|
||||||
|
s: Scalar,
|
||||||
|
) -> (u8, Scalar, Scalar) {
|
||||||
|
let c = EthereumHram::hram(
|
||||||
|
&R,
|
||||||
|
&public_key.A,
|
||||||
|
&[chain_id.to_be_byte_array().as_slice(), &keccak256(m)].concat(),
|
||||||
|
);
|
||||||
|
let sa = -(s * public_key.px);
|
||||||
|
let ca = -(c * public_key.px);
|
||||||
|
(public_key.parity, sa, ca)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ecrecover_hack() {
|
||||||
|
let (keys, public_key) = key_gen();
|
||||||
|
|
||||||
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
|
let hashed_message = keccak256(MESSAGE);
|
||||||
|
let chain_id = U256::ONE;
|
||||||
|
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||||
|
|
||||||
|
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||||
|
let sig = sign(
|
||||||
|
&mut OsRng,
|
||||||
|
&algo,
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, &algo, &keys),
|
||||||
|
full_message,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (parity, sa, ca) =
|
||||||
|
preprocess_signature_for_ecrecover(sig.R, &public_key, chain_id, MESSAGE, sig.s);
|
||||||
|
let q = ecrecover(sa, parity, public_key.px, ca).unwrap();
|
||||||
|
assert_eq!(q, address(&sig.R));
|
||||||
|
}
|
92
coins/ethereum/src/tests/mod.rs
Normal file
92
coins/ethereum/src/tests/mod.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use std::{sync::Arc, time::Duration, fs::File, collections::HashMap};
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use group::ff::PrimeField;
|
||||||
|
use k256::{Scalar, ProjectivePoint};
|
||||||
|
use frost::{curve::Secp256k1, Participant, ThresholdKeys, tests::key_gen as frost_key_gen};
|
||||||
|
|
||||||
|
use ethers_core::{
|
||||||
|
types::{H160, Signature as EthersSignature},
|
||||||
|
abi::Abi,
|
||||||
|
};
|
||||||
|
use ethers_contract::ContractFactory;
|
||||||
|
use ethers_providers::{Middleware, Provider, Http};
|
||||||
|
|
||||||
|
use crate::crypto::PublicKey;
|
||||||
|
|
||||||
|
mod crypto;
|
||||||
|
mod schnorr;
|
||||||
|
mod router;
|
||||||
|
|
||||||
|
pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey) {
|
||||||
|
let mut keys = frost_key_gen::<_, Secp256k1>(&mut OsRng);
|
||||||
|
let mut group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||||
|
|
||||||
|
let mut offset = Scalar::ZERO;
|
||||||
|
while PublicKey::new(group_key).is_none() {
|
||||||
|
offset += Scalar::ONE;
|
||||||
|
group_key += ProjectivePoint::GENERATOR;
|
||||||
|
}
|
||||||
|
for keys in keys.values_mut() {
|
||||||
|
*keys = keys.offset(offset);
|
||||||
|
}
|
||||||
|
let public_key = PublicKey::new(group_key).unwrap();
|
||||||
|
|
||||||
|
(keys, public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace with a contract deployment from an unknown account, so the environment solely has
|
||||||
|
// to fund the deployer, not create/pass a wallet
|
||||||
|
// TODO: Deterministic deployments across chains
|
||||||
|
pub async fn deploy_contract(
|
||||||
|
chain_id: u32,
|
||||||
|
client: Arc<Provider<Http>>,
|
||||||
|
wallet: &k256::ecdsa::SigningKey,
|
||||||
|
name: &str,
|
||||||
|
) -> eyre::Result<H160> {
|
||||||
|
let abi: Abi =
|
||||||
|
serde_json::from_reader(File::open(format!("./artifacts/{name}.abi")).unwrap()).unwrap();
|
||||||
|
|
||||||
|
let hex_bin_buf = std::fs::read_to_string(format!("./artifacts/{name}.bin")).unwrap();
|
||||||
|
let hex_bin =
|
||||||
|
if let Some(stripped) = hex_bin_buf.strip_prefix("0x") { stripped } else { &hex_bin_buf };
|
||||||
|
let bin = hex::decode(hex_bin).unwrap();
|
||||||
|
let factory = ContractFactory::new(abi, bin.into(), client.clone());
|
||||||
|
|
||||||
|
let mut deployment_tx = factory.deploy(())?.tx;
|
||||||
|
deployment_tx.set_chain_id(chain_id);
|
||||||
|
deployment_tx.set_gas(1_000_000);
|
||||||
|
let (max_fee_per_gas, max_priority_fee_per_gas) = client.estimate_eip1559_fees(None).await?;
|
||||||
|
deployment_tx.as_eip1559_mut().unwrap().max_fee_per_gas = Some(max_fee_per_gas);
|
||||||
|
deployment_tx.as_eip1559_mut().unwrap().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
||||||
|
|
||||||
|
let sig_hash = deployment_tx.sighash();
|
||||||
|
let (sig, rid) = wallet.sign_prehash_recoverable(sig_hash.as_ref()).unwrap();
|
||||||
|
|
||||||
|
// EIP-155 v
|
||||||
|
let mut v = u64::from(rid.to_byte());
|
||||||
|
assert!((v == 0) || (v == 1));
|
||||||
|
v += u64::from((chain_id * 2) + 35);
|
||||||
|
|
||||||
|
let r = sig.r().to_repr();
|
||||||
|
let r_ref: &[u8] = r.as_ref();
|
||||||
|
let s = sig.s().to_repr();
|
||||||
|
let s_ref: &[u8] = s.as_ref();
|
||||||
|
let deployment_tx =
|
||||||
|
deployment_tx.rlp_signed(&EthersSignature { r: r_ref.into(), s: s_ref.into(), v });
|
||||||
|
|
||||||
|
let pending_tx = client.send_raw_transaction(deployment_tx).await?;
|
||||||
|
|
||||||
|
let mut receipt;
|
||||||
|
while {
|
||||||
|
receipt = client.get_transaction_receipt(pending_tx.tx_hash()).await?;
|
||||||
|
receipt.is_none()
|
||||||
|
} {
|
||||||
|
tokio::time::sleep(Duration::from_secs(6)).await;
|
||||||
|
}
|
||||||
|
let receipt = receipt.unwrap();
|
||||||
|
assert!(receipt.status == Some(1.into()));
|
||||||
|
|
||||||
|
Ok(receipt.contract_address.unwrap())
|
||||||
|
}
|
109
coins/ethereum/src/tests/router.rs
Normal file
109
coins/ethereum/src/tests/router.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use std::{convert::TryFrom, sync::Arc, collections::HashMap};
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use group::ff::PrimeField;
|
||||||
|
use frost::{
|
||||||
|
curve::Secp256k1,
|
||||||
|
Participant, ThresholdKeys,
|
||||||
|
algorithm::IetfSchnorr,
|
||||||
|
tests::{algorithm_machines, sign},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ethers_core::{
|
||||||
|
types::{H160, U256, Bytes},
|
||||||
|
abi::AbiEncode,
|
||||||
|
utils::{Anvil, AnvilInstance},
|
||||||
|
};
|
||||||
|
use ethers_providers::{Middleware, Provider, Http};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
crypto::{keccak256, PublicKey, EthereumHram, Signature},
|
||||||
|
router::{self, *},
|
||||||
|
tests::{key_gen, deploy_contract},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn setup_test() -> (
|
||||||
|
u32,
|
||||||
|
AnvilInstance,
|
||||||
|
Router<Provider<Http>>,
|
||||||
|
HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
||||||
|
PublicKey,
|
||||||
|
) {
|
||||||
|
let anvil = Anvil::new().spawn();
|
||||||
|
|
||||||
|
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
||||||
|
let chain_id = provider.get_chainid().await.unwrap().as_u32();
|
||||||
|
let wallet = anvil.keys()[0].clone().into();
|
||||||
|
let client = Arc::new(provider);
|
||||||
|
|
||||||
|
let contract_address =
|
||||||
|
deploy_contract(chain_id, client.clone(), &wallet, "Router").await.unwrap();
|
||||||
|
let contract = Router::new(contract_address, client.clone());
|
||||||
|
|
||||||
|
let (keys, public_key) = key_gen();
|
||||||
|
|
||||||
|
// Set the key to the threshold keys
|
||||||
|
let tx = contract.init_serai_key(public_key.px.to_repr().into()).gas(100_000);
|
||||||
|
let pending_tx = tx.send().await.unwrap();
|
||||||
|
let receipt = pending_tx.await.unwrap().unwrap();
|
||||||
|
assert!(receipt.status == Some(1.into()));
|
||||||
|
|
||||||
|
(chain_id, anvil, contract, keys, public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_deploy_contract() {
|
||||||
|
setup_test().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_and_sign(
|
||||||
|
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
chain_id: U256,
|
||||||
|
message: &[u8],
|
||||||
|
) -> Signature {
|
||||||
|
let hashed_message = keccak256(message);
|
||||||
|
|
||||||
|
let mut chain_id_bytes = [0; 32];
|
||||||
|
chain_id.to_big_endian(&mut chain_id_bytes);
|
||||||
|
let full_message = &[chain_id_bytes.as_slice(), &hashed_message].concat();
|
||||||
|
|
||||||
|
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||||
|
let sig = sign(
|
||||||
|
&mut OsRng,
|
||||||
|
&algo,
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, &algo, keys),
|
||||||
|
full_message,
|
||||||
|
);
|
||||||
|
|
||||||
|
Signature::new(public_key, k256::U256::from_words(chain_id.0), message, sig).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_router_execute() {
|
||||||
|
let (chain_id, _anvil, contract, keys, public_key) = setup_test().await;
|
||||||
|
|
||||||
|
let to = H160([0u8; 20]);
|
||||||
|
let value = U256([0u64; 4]);
|
||||||
|
let data = Bytes::from([0]);
|
||||||
|
let tx = OutInstruction { to, value, data: data.clone() };
|
||||||
|
|
||||||
|
let nonce_call = contract.nonce();
|
||||||
|
let nonce = nonce_call.call().await.unwrap();
|
||||||
|
|
||||||
|
let encoded =
|
||||||
|
("execute".to_string(), nonce, vec![router::OutInstruction { to, value, data }]).encode();
|
||||||
|
let sig = hash_and_sign(&keys, &public_key, chain_id.into(), &encoded);
|
||||||
|
|
||||||
|
let tx = contract
|
||||||
|
.execute(vec![tx], router::Signature { c: sig.c.to_repr().into(), s: sig.s.to_repr().into() })
|
||||||
|
.gas(300_000);
|
||||||
|
let pending_tx = tx.send().await.unwrap();
|
||||||
|
let receipt = dbg!(pending_tx.await.unwrap().unwrap());
|
||||||
|
assert!(receipt.status == Some(1.into()));
|
||||||
|
|
||||||
|
println!("gas used: {:?}", receipt.cumulative_gas_used);
|
||||||
|
println!("logs: {:?}", receipt.logs);
|
||||||
|
}
|
67
coins/ethereum/src/tests/schnorr.rs
Normal file
67
coins/ethereum/src/tests/schnorr.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use std::{convert::TryFrom, sync::Arc};
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use ::k256::{elliptic_curve::bigint::ArrayEncoding, U256, Scalar};
|
||||||
|
|
||||||
|
use ethers_core::utils::{keccak256, Anvil, AnvilInstance};
|
||||||
|
use ethers_providers::{Middleware, Provider, Http};
|
||||||
|
|
||||||
|
use frost::{
|
||||||
|
curve::Secp256k1,
|
||||||
|
algorithm::IetfSchnorr,
|
||||||
|
tests::{algorithm_machines, sign},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
crypto::*,
|
||||||
|
schnorr::*,
|
||||||
|
tests::{key_gen, deploy_contract},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn setup_test() -> (u32, AnvilInstance, Schnorr<Provider<Http>>) {
|
||||||
|
let anvil = Anvil::new().spawn();
|
||||||
|
|
||||||
|
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
||||||
|
let chain_id = provider.get_chainid().await.unwrap().as_u32();
|
||||||
|
let wallet = anvil.keys()[0].clone().into();
|
||||||
|
let client = Arc::new(provider);
|
||||||
|
|
||||||
|
let contract_address =
|
||||||
|
deploy_contract(chain_id, client.clone(), &wallet, "Schnorr").await.unwrap();
|
||||||
|
let contract = Schnorr::new(contract_address, client.clone());
|
||||||
|
(chain_id, anvil, contract)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_deploy_contract() {
|
||||||
|
setup_test().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_ecrecover_hack() {
|
||||||
|
let (chain_id, _anvil, contract) = setup_test().await;
|
||||||
|
let chain_id = U256::from(chain_id);
|
||||||
|
|
||||||
|
let (keys, public_key) = key_gen();
|
||||||
|
|
||||||
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
|
let hashed_message = keccak256(MESSAGE);
|
||||||
|
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||||
|
|
||||||
|
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||||
|
let sig = sign(
|
||||||
|
&mut OsRng,
|
||||||
|
&algo,
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, &algo, &keys),
|
||||||
|
full_message,
|
||||||
|
);
|
||||||
|
let sig = Signature::new(&public_key, chain_id, MESSAGE, sig).unwrap();
|
||||||
|
|
||||||
|
call_verify(&contract, &public_key, MESSAGE, &sig).await.unwrap();
|
||||||
|
// Test an invalid signature fails
|
||||||
|
let mut sig = sig;
|
||||||
|
sig.s += Scalar::ONE;
|
||||||
|
assert!(call_verify(&contract, &public_key, MESSAGE, &sig).await.is_err());
|
||||||
|
}
|
|
@ -1,128 +0,0 @@
|
||||||
use std::{convert::TryFrom, sync::Arc, time::Duration, fs::File};
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use ::k256::{
|
|
||||||
elliptic_curve::{bigint::ArrayEncoding, PrimeField},
|
|
||||||
U256,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ethers_core::{
|
|
||||||
types::Signature,
|
|
||||||
abi::Abi,
|
|
||||||
utils::{keccak256, Anvil, AnvilInstance},
|
|
||||||
};
|
|
||||||
use ethers_contract::ContractFactory;
|
|
||||||
use ethers_providers::{Middleware, Provider, Http};
|
|
||||||
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
Participant,
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{key_gen, algorithm_machines, sign},
|
|
||||||
};
|
|
||||||
|
|
||||||
use ethereum_serai::{
|
|
||||||
crypto,
|
|
||||||
contract::{Schnorr, call_verify},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Replace with a contract deployment from an unknown account, so the environment solely has
|
|
||||||
// to fund the deployer, not create/pass a wallet
|
|
||||||
pub async fn deploy_schnorr_verifier_contract(
|
|
||||||
chain_id: u32,
|
|
||||||
client: Arc<Provider<Http>>,
|
|
||||||
wallet: &k256::ecdsa::SigningKey,
|
|
||||||
) -> eyre::Result<Schnorr<Provider<Http>>> {
|
|
||||||
let abi: Abi = serde_json::from_reader(File::open("./artifacts/Schnorr.abi").unwrap()).unwrap();
|
|
||||||
|
|
||||||
let hex_bin_buf = std::fs::read_to_string("./artifacts/Schnorr.bin").unwrap();
|
|
||||||
let hex_bin =
|
|
||||||
if let Some(stripped) = hex_bin_buf.strip_prefix("0x") { stripped } else { &hex_bin_buf };
|
|
||||||
let bin = hex::decode(hex_bin).unwrap();
|
|
||||||
let factory = ContractFactory::new(abi, bin.into(), client.clone());
|
|
||||||
|
|
||||||
let mut deployment_tx = factory.deploy(())?.tx;
|
|
||||||
deployment_tx.set_chain_id(chain_id);
|
|
||||||
deployment_tx.set_gas(500_000);
|
|
||||||
let (max_fee_per_gas, max_priority_fee_per_gas) = client.estimate_eip1559_fees(None).await?;
|
|
||||||
deployment_tx.as_eip1559_mut().unwrap().max_fee_per_gas = Some(max_fee_per_gas);
|
|
||||||
deployment_tx.as_eip1559_mut().unwrap().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
|
||||||
|
|
||||||
let sig_hash = deployment_tx.sighash();
|
|
||||||
let (sig, rid) = wallet.sign_prehash_recoverable(sig_hash.as_ref()).unwrap();
|
|
||||||
|
|
||||||
// EIP-155 v
|
|
||||||
let mut v = u64::from(rid.to_byte());
|
|
||||||
assert!((v == 0) || (v == 1));
|
|
||||||
v += u64::from((chain_id * 2) + 35);
|
|
||||||
|
|
||||||
let r = sig.r().to_repr();
|
|
||||||
let r_ref: &[u8] = r.as_ref();
|
|
||||||
let s = sig.s().to_repr();
|
|
||||||
let s_ref: &[u8] = s.as_ref();
|
|
||||||
let deployment_tx = deployment_tx.rlp_signed(&Signature { r: r_ref.into(), s: s_ref.into(), v });
|
|
||||||
|
|
||||||
let pending_tx = client.send_raw_transaction(deployment_tx).await?;
|
|
||||||
|
|
||||||
let mut receipt;
|
|
||||||
while {
|
|
||||||
receipt = client.get_transaction_receipt(pending_tx.tx_hash()).await?;
|
|
||||||
receipt.is_none()
|
|
||||||
} {
|
|
||||||
tokio::time::sleep(Duration::from_secs(6)).await;
|
|
||||||
}
|
|
||||||
let receipt = receipt.unwrap();
|
|
||||||
assert!(receipt.status == Some(1.into()));
|
|
||||||
|
|
||||||
let contract = Schnorr::new(receipt.contract_address.unwrap(), client.clone());
|
|
||||||
Ok(contract)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn deploy_test_contract() -> (u32, AnvilInstance, Schnorr<Provider<Http>>) {
|
|
||||||
let anvil = Anvil::new().spawn();
|
|
||||||
|
|
||||||
let provider =
|
|
||||||
Provider::<Http>::try_from(anvil.endpoint()).unwrap().interval(Duration::from_millis(10u64));
|
|
||||||
let chain_id = provider.get_chainid().await.unwrap().as_u32();
|
|
||||||
let wallet = anvil.keys()[0].clone().into();
|
|
||||||
let client = Arc::new(provider);
|
|
||||||
|
|
||||||
(chain_id, anvil, deploy_schnorr_verifier_contract(chain_id, client, &wallet).await.unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_deploy_contract() {
|
|
||||||
deploy_test_contract().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_ecrecover_hack() {
|
|
||||||
let (chain_id, _anvil, contract) = deploy_test_contract().await;
|
|
||||||
let chain_id = U256::from(chain_id);
|
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let hashed_message = keccak256(MESSAGE);
|
|
||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, crypto::EthereumHram>::ietf();
|
|
||||||
let sig = sign(
|
|
||||||
&mut OsRng,
|
|
||||||
&algo,
|
|
||||||
keys.clone(),
|
|
||||||
algorithm_machines(&mut OsRng, &algo, &keys),
|
|
||||||
full_message,
|
|
||||||
);
|
|
||||||
let mut processed_sig =
|
|
||||||
crypto::process_signature_for_contract(hashed_message, &sig.R, sig.s, &group_key, chain_id);
|
|
||||||
|
|
||||||
call_verify(&contract, &processed_sig).await.unwrap();
|
|
||||||
|
|
||||||
// test invalid signature fails
|
|
||||||
processed_sig.message[0] = 0;
|
|
||||||
assert!(call_verify(&contract, &processed_sig).await.is_err());
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
use k256::{
|
|
||||||
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, sec1::ToEncodedPoint},
|
|
||||||
ProjectivePoint, Scalar, U256,
|
|
||||||
};
|
|
||||||
use frost::{curve::Secp256k1, Participant};
|
|
||||||
|
|
||||||
use ethereum_serai::crypto::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover() {
|
|
||||||
use rand_core::OsRng;
|
|
||||||
use sha2::Sha256;
|
|
||||||
use sha3::{Digest, Keccak256};
|
|
||||||
use k256::ecdsa::{hazmat::SignPrimitive, signature::DigestVerifier, SigningKey, VerifyingKey};
|
|
||||||
|
|
||||||
let private = SigningKey::random(&mut OsRng);
|
|
||||||
let public = VerifyingKey::from(&private);
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let (sig, recovery_id) = private
|
|
||||||
.as_nonzero_scalar()
|
|
||||||
.try_sign_prehashed_rfc6979::<Sha256>(&Keccak256::digest(MESSAGE), b"")
|
|
||||||
.unwrap();
|
|
||||||
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
|
||||||
{
|
|
||||||
assert_eq!(public.verify_digest(Keccak256::new_with_prefix(MESSAGE), &sig).unwrap(), ());
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ecrecover(hash_to_scalar(MESSAGE), recovery_id.unwrap().is_y_odd().into(), *sig.r(), *sig.s())
|
|
||||||
.unwrap(),
|
|
||||||
address(&ProjectivePoint::from(public.as_affine()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_signing() {
|
|
||||||
use frost::{
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
|
||||||
};
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
let _group_key = keys[&Participant::new(1).unwrap()].group_key();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover_hack() {
|
|
||||||
use frost::{
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
|
||||||
};
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
let group_key = keys[&Participant::new(1).unwrap()].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::reduce(U256::from_be_slice(&group_key_compressed[1 .. 33]));
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let hashed_message = keccak256(MESSAGE);
|
|
||||||
let chain_id = U256::ONE;
|
|
||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let sig = sign(
|
|
||||||
&mut OsRng,
|
|
||||||
&algo,
|
|
||||||
keys.clone(),
|
|
||||||
algorithm_machines(&mut OsRng, &algo, &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));
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
mod contract;
|
|
||||||
mod crypto;
|
|
|
@ -36,16 +36,16 @@ rustup target add wasm32-unknown-unknown --toolchain nightly
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo install svm-rs
|
cargo install svm-rs
|
||||||
svm install 0.8.16
|
svm install 0.8.25
|
||||||
svm use 0.8.16
|
svm use 0.8.25
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Solidity Compiler Version Manager
|
### Install Solidity Compiler Version Manager
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo install svm-rs
|
cargo install svm-rs
|
||||||
svm install 0.8.16
|
svm install 0.8.25
|
||||||
svm use 0.8.16
|
svm use 0.8.25
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install foundry (for tests)
|
### Install foundry (for tests)
|
||||||
|
|
Loading…
Reference in a new issue