Skip to content

Commit fab1c35

Browse files
TomAFrenchvezenovm
andauthored
feat: add native rust implementation of schnorr signature verification (#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 <[email protected]>
1 parent af57471 commit fab1c35

9 files changed

Lines changed: 236 additions & 1202 deletions

File tree

Cargo.lock

Lines changed: 71 additions & 723 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

acvm-repo/acvm_js/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ tracing-web.workspace = true
2929

3030
const-str = "0.5.5"
3131

32+
# This is an unused dependency, we are adding it
33+
# so that we can enable the js feature in getrandom.
34+
getrandom = { workspace = true, features = ["js"] }
35+
3236
[build-dependencies]
3337
build-data.workspace = true
3438
pkg-config = "0.3"

acvm-repo/acvm_js/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]
44

5+
// See Cargo.toml for explanation.
6+
use getrandom as _;
7+
58
mod black_box_solvers;
69
mod build_info;
710
mod compression;

acvm-repo/bn254_blackbox_solver/Cargo.toml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ repository.workspace = true
1515
[dependencies]
1616
acir.workspace = true
1717
acvm_blackbox_solver.workspace = true
18-
thiserror.workspace = true
19-
cfg-if = "1.0.0"
2018
hex.workspace = true
2119
lazy_static = "1.4"
2220

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

29-
[target.'cfg(target_arch = "wasm32")'.dependencies]
30-
wasmer = { version = "4.2.6", default-features = false, features = [
31-
"js-default",
32-
] }
33-
34-
getrandom = { workspace = true, features = ["js"] }
35-
wasm-bindgen-futures.workspace = true
36-
js-sys.workspace = true
37-
38-
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
39-
getrandom.workspace = true
40-
wasmer = "4.2.6"
41-
4227
[dev-dependencies]
4328
ark-std = { version = "^0.4.0", default-features = false }
4429
criterion = "0.5.0"

acvm-repo/bn254_blackbox_solver/src/lib.rs

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,29 @@
22
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]
44

5-
use acir::{BlackBoxFunc, FieldElement};
5+
use acir::FieldElement;
66
use acvm_blackbox_solver::{BlackBoxFunctionSolver, BlackBoxResolutionError};
77

88
mod embedded_curve_ops;
99
mod generator;
1010
mod pedersen;
1111
mod poseidon2;
12-
mod wasm;
12+
mod schnorr;
1313

1414
use ark_ec::AffineRepr;
1515
pub use embedded_curve_ops::{embedded_curve_add, multi_scalar_mul};
1616
pub use poseidon2::poseidon2_permutation;
17-
use wasm::Barretenberg;
1817

19-
use self::wasm::SchnorrSig;
20-
21-
pub struct Bn254BlackBoxSolver {
22-
blackbox_vendor: Barretenberg,
23-
}
18+
pub struct Bn254BlackBoxSolver;
2419

2520
impl Bn254BlackBoxSolver {
2621
pub async fn initialize() -> Bn254BlackBoxSolver {
27-
// We fallback to the sync initialization of barretenberg on non-wasm targets.
28-
// This ensures that wasm packages consuming this still build on the default target (useful for linting, etc.)
29-
cfg_if::cfg_if! {
30-
if #[cfg(target_arch = "wasm32")] {
31-
let blackbox_vendor = Barretenberg::initialize().await;
32-
Bn254BlackBoxSolver { blackbox_vendor }
33-
} else {
34-
Bn254BlackBoxSolver::new()
35-
}
36-
}
22+
Bn254BlackBoxSolver
3723
}
3824

3925
#[cfg(not(target_arch = "wasm32"))]
4026
pub fn new() -> Bn254BlackBoxSolver {
41-
let blackbox_vendor = Barretenberg::new();
42-
Bn254BlackBoxSolver { blackbox_vendor }
27+
Bn254BlackBoxSolver
4328
}
4429
}
4530

@@ -58,16 +43,15 @@ impl BlackBoxFunctionSolver for Bn254BlackBoxSolver {
5843
signature: &[u8; 64],
5944
message: &[u8],
6045
) -> Result<bool, BlackBoxResolutionError> {
61-
let pub_key_bytes: Vec<u8> =
62-
public_key_x.to_be_bytes().iter().copied().chain(public_key_y.to_be_bytes()).collect();
63-
64-
let pub_key: [u8; 64] = pub_key_bytes.try_into().unwrap();
6546
let sig_s: [u8; 32] = signature[0..32].try_into().unwrap();
6647
let sig_e: [u8; 32] = signature[32..64].try_into().unwrap();
67-
68-
self.blackbox_vendor.verify_signature(pub_key, sig_s, sig_e, message).map_err(|err| {
69-
BlackBoxResolutionError::Failed(BlackBoxFunc::SchnorrVerify, err.to_string())
70-
})
48+
Ok(schnorr::verify_signature(
49+
public_key_x.into_repr(),
50+
public_key_y.into_repr(),
51+
sig_s,
52+
sig_e,
53+
message,
54+
))
7155
}
7256

7357
fn pedersen_commitment(
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use acvm_blackbox_solver::blake2s;
2+
use ark_ec::{
3+
short_weierstrass::{Affine, SWCurveConfig},
4+
AffineRepr, CurveConfig, CurveGroup,
5+
};
6+
use ark_ff::{BigInteger, PrimeField, Zero};
7+
use grumpkin::{Fq, GrumpkinParameters};
8+
9+
pub(crate) fn verify_signature(
10+
pub_key_x: Fq,
11+
pub_key_y: Fq,
12+
sig_s_bytes: [u8; 32],
13+
sig_e_bytes: [u8; 32],
14+
message: &[u8],
15+
) -> bool {
16+
let pub_key = Affine::<GrumpkinParameters>::new_unchecked(pub_key_x, pub_key_y);
17+
18+
if !pub_key.is_on_curve()
19+
|| !pub_key.is_in_correct_subgroup_assuming_on_curve()
20+
|| pub_key.is_zero()
21+
{
22+
return false;
23+
}
24+
25+
let sig_s =
26+
<GrumpkinParameters as CurveConfig>::ScalarField::from_be_bytes_mod_order(&sig_s_bytes);
27+
let sig_e =
28+
<GrumpkinParameters as CurveConfig>::ScalarField::from_be_bytes_mod_order(&sig_e_bytes);
29+
30+
if sig_s.is_zero() || sig_e.is_zero() {
31+
return false;
32+
}
33+
34+
// R = g^{sig.s} • pub^{sig.e}
35+
let r = GrumpkinParameters::GENERATOR * sig_s + pub_key * sig_e;
36+
if r.is_zero() {
37+
// this result implies k == 0, which would be catastrophic for the prover.
38+
// it is a cheap check that ensures this doesn't happen.
39+
return false;
40+
}
41+
42+
// compare the _hashes_ rather than field elements modulo r
43+
// e = H(pedersen(r, pk.x, pk.y), m), where r = R.x
44+
let target_e_bytes = schnorr_generate_challenge(message, pub_key_x, pub_key_y, r.into_affine());
45+
46+
sig_e_bytes == target_e_bytes
47+
}
48+
49+
fn schnorr_generate_challenge(
50+
message: &[u8],
51+
pub_key_x: Fq,
52+
pub_key_y: Fq,
53+
r: Affine<GrumpkinParameters>,
54+
) -> [u8; 32] {
55+
// create challenge message pedersen_commitment(R.x, pubkey)
56+
57+
let r_x = *r.x().expect("r has been checked to be non-zero");
58+
let pedersen_hash = crate::pedersen::hash::hash_with_index(&[r_x, pub_key_x, pub_key_y], 0);
59+
60+
let mut hash_input: Vec<u8> = pedersen_hash.into_bigint().to_bytes_be();
61+
hash_input.extend(message);
62+
63+
blake2s(&hash_input).unwrap()
64+
}
65+
66+
#[cfg(test)]
67+
mod schnorr_tests {
68+
use acir::FieldElement;
69+
70+
use super::verify_signature;
71+
72+
#[test]
73+
fn verifies_valid_signature() {
74+
let pub_key_x: grumpkin::Fq = FieldElement::from_hex(
75+
"0x04b260954662e97f00cab9adb773a259097f7a274b83b113532bce27fa3fb96a",
76+
)
77+
.unwrap()
78+
.into_repr();
79+
let pub_key_y: grumpkin::Fq = FieldElement::from_hex(
80+
"0x2fd51571db6c08666b0edfbfbc57d432068bccd0110a39b166ab243da0037197",
81+
)
82+
.unwrap()
83+
.into_repr();
84+
let sig_s_bytes: [u8; 32] = [
85+
1, 13, 119, 112, 212, 39, 233, 41, 84, 235, 255, 93, 245, 172, 186, 83, 157, 253, 76,
86+
77, 33, 128, 178, 15, 214, 67, 105, 107, 177, 234, 77, 48,
87+
];
88+
let sig_e_bytes: [u8; 32] = [
89+
27, 237, 155, 84, 39, 84, 247, 27, 22, 8, 176, 230, 24, 115, 145, 220, 254, 122, 135,
90+
179, 171, 4, 214, 202, 64, 199, 19, 84, 239, 138, 124, 12,
91+
];
92+
let message: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
93+
94+
assert!(verify_signature(pub_key_x, pub_key_y, sig_s_bytes, sig_e_bytes, message));
95+
}
96+
97+
#[test]
98+
fn rejects_zero_e() {
99+
let pub_key_x: grumpkin::Fq = FieldElement::from_hex(
100+
"0x04b260954662e97f00cab9adb773a259097f7a274b83b113532bce27fa3fb96a",
101+
)
102+
.unwrap()
103+
.into_repr();
104+
let pub_key_y: grumpkin::Fq = FieldElement::from_hex(
105+
"0x2fd51571db6c08666b0edfbfbc57d432068bccd0110a39b166ab243da0037197",
106+
)
107+
.unwrap()
108+
.into_repr();
109+
let sig_s_bytes: [u8; 32] = [
110+
1, 13, 119, 112, 212, 39, 233, 41, 84, 235, 255, 93, 245, 172, 186, 83, 157, 253, 76,
111+
77, 33, 128, 178, 15, 214, 67, 105, 107, 177, 234, 77, 48,
112+
];
113+
let sig_e_bytes: [u8; 32] = [
114+
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,
115+
0, 0, 0,
116+
];
117+
let message: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
118+
119+
assert!(!verify_signature(pub_key_x, pub_key_y, sig_s_bytes, sig_e_bytes, message));
120+
}
121+
122+
#[test]
123+
fn rejects_zero_s() {
124+
let pub_key_x: grumpkin::Fq = FieldElement::from_hex(
125+
"0x04b260954662e97f00cab9adb773a259097f7a274b83b113532bce27fa3fb96a",
126+
)
127+
.unwrap()
128+
.into_repr();
129+
let pub_key_y: grumpkin::Fq = FieldElement::from_hex(
130+
"0x2fd51571db6c08666b0edfbfbc57d432068bccd0110a39b166ab243da0037197",
131+
)
132+
.unwrap()
133+
.into_repr();
134+
let sig_s_bytes: [u8; 32] = [
135+
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,
136+
0, 0, 0,
137+
];
138+
let sig_e_bytes: [u8; 32] = [
139+
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,
140+
0, 0, 0,
141+
];
142+
let message: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
143+
144+
assert!(!verify_signature(pub_key_x, pub_key_y, sig_s_bytes, sig_e_bytes, message));
145+
}
146+
}
-738 KB
Binary file not shown.

0 commit comments

Comments
 (0)