Skip to content

Commit

Permalink
feat: add native rust implementation of schnorr signature verification (
Browse files Browse the repository at this point in the history
#5053)

# Description

## Problem\*

Resolves <!-- Link to GitHub Issue -->

## Summary\*

This PR replaces the final wasm calls to barretenberg with a native
implementation of schnorr signature verification. This allows us to
remove the entire `acvm_backend.wasm`.

This schnorr implementation is something this I've slapped together in
an afternoon so be warned of potential bugs, there doesn't seem to be
any off-the-shelf rust implementations of schnorr using grumpkin
however.

Now we don't need to do wasm initialisation for acvm_js, we can stop
caching a solver object to pass into acvm_js, I've maintained the
interface for now however but the external solver is ignored.


Benchmarks relative to #5056
```
schnorr_verify          time:   [526.91 µs 527.81 µs 528.91 µs]
                        change: [-67.185% -67.081% -66.993%] (p = 0.00 < 0.05)
                        Performance has improved.
```

## Additional Context



## Documentation\*

Check one:
- [x] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.

---------

Co-authored-by: Maxim Vezenov <mvezenov@gmail.com>
  • Loading branch information
TomAFrench and vezenovm authored May 21, 2024
1 parent af57471 commit fab1c35
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 1,202 deletions.
794 changes: 71 additions & 723 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions acvm-repo/acvm_js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ tracing-web.workspace = true

const-str = "0.5.5"

# This is an unused dependency, we are adding it
# so that we can enable the js feature in getrandom.
getrandom = { workspace = true, features = ["js"] }

[build-dependencies]
build-data.workspace = true
pkg-config = "0.3"
Expand Down
3 changes: 3 additions & 0 deletions acvm-repo/acvm_js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#![warn(clippy::semicolon_if_nothing_returned)]
#![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]

// See Cargo.toml for explanation.
use getrandom as _;

mod black_box_solvers;
mod build_info;
mod compression;
Expand Down
15 changes: 0 additions & 15 deletions acvm-repo/bn254_blackbox_solver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ repository.workspace = true
[dependencies]
acir.workspace = true
acvm_blackbox_solver.workspace = true
thiserror.workspace = true
cfg-if = "1.0.0"
hex.workspace = true
lazy_static = "1.4"

Expand All @@ -26,19 +24,6 @@ ark-ec = { version = "^0.4.0", default-features = false }
ark-ff = { version = "^0.4.0", default-features = false }
num-bigint.workspace = true

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasmer = { version = "4.2.6", default-features = false, features = [
"js-default",
] }

getrandom = { workspace = true, features = ["js"] }
wasm-bindgen-futures.workspace = true
js-sys.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom.workspace = true
wasmer = "4.2.6"

[dev-dependencies]
ark-std = { version = "^0.4.0", default-features = false }
criterion = "0.5.0"
Expand Down
40 changes: 12 additions & 28 deletions acvm-repo/bn254_blackbox_solver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,29 @@
#![warn(clippy::semicolon_if_nothing_returned)]
#![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]

use acir::{BlackBoxFunc, FieldElement};
use acir::FieldElement;
use acvm_blackbox_solver::{BlackBoxFunctionSolver, BlackBoxResolutionError};

mod embedded_curve_ops;
mod generator;
mod pedersen;
mod poseidon2;
mod wasm;
mod schnorr;

use ark_ec::AffineRepr;
pub use embedded_curve_ops::{embedded_curve_add, multi_scalar_mul};
pub use poseidon2::poseidon2_permutation;
use wasm::Barretenberg;

use self::wasm::SchnorrSig;

pub struct Bn254BlackBoxSolver {
blackbox_vendor: Barretenberg,
}
pub struct Bn254BlackBoxSolver;

impl Bn254BlackBoxSolver {
pub async fn initialize() -> Bn254BlackBoxSolver {
// We fallback to the sync initialization of barretenberg on non-wasm targets.
// This ensures that wasm packages consuming this still build on the default target (useful for linting, etc.)
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
let blackbox_vendor = Barretenberg::initialize().await;
Bn254BlackBoxSolver { blackbox_vendor }
} else {
Bn254BlackBoxSolver::new()
}
}
Bn254BlackBoxSolver
}

#[cfg(not(target_arch = "wasm32"))]
pub fn new() -> Bn254BlackBoxSolver {
let blackbox_vendor = Barretenberg::new();
Bn254BlackBoxSolver { blackbox_vendor }
Bn254BlackBoxSolver
}
}

Expand All @@ -58,16 +43,15 @@ impl BlackBoxFunctionSolver for Bn254BlackBoxSolver {
signature: &[u8; 64],
message: &[u8],
) -> Result<bool, BlackBoxResolutionError> {
let pub_key_bytes: Vec<u8> =
public_key_x.to_be_bytes().iter().copied().chain(public_key_y.to_be_bytes()).collect();

let pub_key: [u8; 64] = pub_key_bytes.try_into().unwrap();
let sig_s: [u8; 32] = signature[0..32].try_into().unwrap();
let sig_e: [u8; 32] = signature[32..64].try_into().unwrap();

self.blackbox_vendor.verify_signature(pub_key, sig_s, sig_e, message).map_err(|err| {
BlackBoxResolutionError::Failed(BlackBoxFunc::SchnorrVerify, err.to_string())
})
Ok(schnorr::verify_signature(
public_key_x.into_repr(),
public_key_y.into_repr(),
sig_s,
sig_e,
message,
))
}

fn pedersen_commitment(
Expand Down
146 changes: 146 additions & 0 deletions acvm-repo/bn254_blackbox_solver/src/schnorr/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use acvm_blackbox_solver::blake2s;
use ark_ec::{
short_weierstrass::{Affine, SWCurveConfig},
AffineRepr, CurveConfig, CurveGroup,
};
use ark_ff::{BigInteger, PrimeField, Zero};
use grumpkin::{Fq, GrumpkinParameters};

pub(crate) fn verify_signature(
pub_key_x: Fq,
pub_key_y: Fq,
sig_s_bytes: [u8; 32],
sig_e_bytes: [u8; 32],
message: &[u8],
) -> bool {
let pub_key = Affine::<GrumpkinParameters>::new_unchecked(pub_key_x, pub_key_y);

if !pub_key.is_on_curve()
|| !pub_key.is_in_correct_subgroup_assuming_on_curve()
|| pub_key.is_zero()
{
return false;
}

let sig_s =
<GrumpkinParameters as CurveConfig>::ScalarField::from_be_bytes_mod_order(&sig_s_bytes);
let sig_e =
<GrumpkinParameters as CurveConfig>::ScalarField::from_be_bytes_mod_order(&sig_e_bytes);

if sig_s.is_zero() || sig_e.is_zero() {
return false;
}

// R = g^{sig.s} • pub^{sig.e}
let r = GrumpkinParameters::GENERATOR * sig_s + pub_key * sig_e;
if r.is_zero() {
// this result implies k == 0, which would be catastrophic for the prover.
// it is a cheap check that ensures this doesn't happen.
return false;
}

// compare the _hashes_ rather than field elements modulo r
// e = H(pedersen(r, pk.x, pk.y), m), where r = R.x
let target_e_bytes = schnorr_generate_challenge(message, pub_key_x, pub_key_y, r.into_affine());

sig_e_bytes == target_e_bytes
}

fn schnorr_generate_challenge(
message: &[u8],
pub_key_x: Fq,
pub_key_y: Fq,
r: Affine<GrumpkinParameters>,
) -> [u8; 32] {
// create challenge message pedersen_commitment(R.x, pubkey)

let r_x = *r.x().expect("r has been checked to be non-zero");
let pedersen_hash = crate::pedersen::hash::hash_with_index(&[r_x, pub_key_x, pub_key_y], 0);

let mut hash_input: Vec<u8> = pedersen_hash.into_bigint().to_bytes_be();
hash_input.extend(message);

blake2s(&hash_input).unwrap()
}

#[cfg(test)]
mod schnorr_tests {
use acir::FieldElement;

use super::verify_signature;

#[test]
fn verifies_valid_signature() {
let pub_key_x: grumpkin::Fq = FieldElement::from_hex(
"0x04b260954662e97f00cab9adb773a259097f7a274b83b113532bce27fa3fb96a",
)
.unwrap()
.into_repr();
let pub_key_y: grumpkin::Fq = FieldElement::from_hex(
"0x2fd51571db6c08666b0edfbfbc57d432068bccd0110a39b166ab243da0037197",
)
.unwrap()
.into_repr();
let sig_s_bytes: [u8; 32] = [
1, 13, 119, 112, 212, 39, 233, 41, 84, 235, 255, 93, 245, 172, 186, 83, 157, 253, 76,
77, 33, 128, 178, 15, 214, 67, 105, 107, 177, 234, 77, 48,
];
let sig_e_bytes: [u8; 32] = [
27, 237, 155, 84, 39, 84, 247, 27, 22, 8, 176, 230, 24, 115, 145, 220, 254, 122, 135,
179, 171, 4, 214, 202, 64, 199, 19, 84, 239, 138, 124, 12,
];
let message: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

assert!(verify_signature(pub_key_x, pub_key_y, sig_s_bytes, sig_e_bytes, message));
}

#[test]
fn rejects_zero_e() {
let pub_key_x: grumpkin::Fq = FieldElement::from_hex(
"0x04b260954662e97f00cab9adb773a259097f7a274b83b113532bce27fa3fb96a",
)
.unwrap()
.into_repr();
let pub_key_y: grumpkin::Fq = FieldElement::from_hex(
"0x2fd51571db6c08666b0edfbfbc57d432068bccd0110a39b166ab243da0037197",
)
.unwrap()
.into_repr();
let sig_s_bytes: [u8; 32] = [
1, 13, 119, 112, 212, 39, 233, 41, 84, 235, 255, 93, 245, 172, 186, 83, 157, 253, 76,
77, 33, 128, 178, 15, 214, 67, 105, 107, 177, 234, 77, 48,
];
let sig_e_bytes: [u8; 32] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
];
let message: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

assert!(!verify_signature(pub_key_x, pub_key_y, sig_s_bytes, sig_e_bytes, message));
}

#[test]
fn rejects_zero_s() {
let pub_key_x: grumpkin::Fq = FieldElement::from_hex(
"0x04b260954662e97f00cab9adb773a259097f7a274b83b113532bce27fa3fb96a",
)
.unwrap()
.into_repr();
let pub_key_y: grumpkin::Fq = FieldElement::from_hex(
"0x2fd51571db6c08666b0edfbfbc57d432068bccd0110a39b166ab243da0037197",
)
.unwrap()
.into_repr();
let sig_s_bytes: [u8; 32] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
];
let sig_e_bytes: [u8; 32] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
];
let message: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

assert!(!verify_signature(pub_key_x, pub_key_y, sig_s_bytes, sig_e_bytes, message));
}
}
Binary file not shown.
Loading

0 comments on commit fab1c35

Please sign in to comment.