Skip to content

Commit b63ee82

Browse files
authored
feat: enable verifying compressed gnark groth16 proof (#2454)
1 parent 5868f0b commit b63ee82

File tree

10 files changed

+310
-11
lines changed

10 files changed

+310
-11
lines changed

crates/test-artifacts/build.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,23 @@ fn main() -> Result<()> {
3131
build_program_with_args(
3232
"../verifier/guest-verify-programs",
3333
BuildArgs {
34-
binaries: vec!["groth16_verify".to_string(), "plonk_verify".to_string()],
34+
binaries: vec![
35+
"groth16_verify".to_string(),
36+
"groth16_verify_compressed".to_string(),
37+
"plonk_verify".to_string(),
38+
],
3539
..Default::default()
3640
},
3741
);
3842

3943
build_program_with_args(
4044
"../verifier/guest-verify-programs",
4145
BuildArgs {
42-
binaries: vec!["groth16_verify_blake3".to_string(), "plonk_verify_blake3".to_string()],
46+
binaries: vec![
47+
"groth16_verify_blake3".to_string(),
48+
"groth16_verify_compressed_blake3".to_string(),
49+
"plonk_verify_blake3".to_string(),
50+
],
4351
features: vec!["blake3".to_string()],
4452
..Default::default()
4553
},

crates/test-artifacts/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ pub const GROTH16_ELF: &[u8] = include_elf!("groth16_verify");
8888

8989
pub const GROTH16_BLAKE3_ELF: &[u8] = include_elf!("groth16_verify_blake3");
9090

91+
pub const GROTH16_COMPRESSED_ELF: &[u8] = include_elf!("groth16_verify_compressed");
92+
93+
pub const GROTH16_COMPRESSED_BLAKE3_ELF: &[u8] = include_elf!("groth16_verify_compressed_blake3");
94+
9195
pub const PLONK_ELF: &[u8] = include_elf!("plonk_verify");
9296

9397
pub const PLONK_BLAKE3_ELF: &[u8] = include_elf!("plonk_verify_blake3");

crates/verifier/guest-verify-programs/Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ name = "groth16_verify_blake3"
1313
path = "src/groth16_verify.rs"
1414
required-features = ["blake3"]
1515

16+
[[bin]]
17+
name = "groth16_verify_compressed"
18+
path = "src/groth16_verify_compressed.rs"
19+
20+
[[bin]]
21+
name = "groth16_verify_compressed_blake3"
22+
path = "src/groth16_verify_compressed.rs"
23+
required-features = ["blake3"]
24+
1625
[[bin]]
1726
name = "plonk_verify"
1827
path = "src/plonk_verify.rs"
@@ -27,4 +36,4 @@ sp1-zkvm = { path = "../../zkvm/entrypoint" }
2736
sp1-verifier = { path = "../" }
2837

2938
[features]
30-
blake3 = ["sp1-zkvm/blake3"]
39+
blake3 = ["sp1-zkvm/blake3"]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#![no_main]
2+
sp1_zkvm::entrypoint!(main);
3+
4+
use sp1_verifier::Groth16Verifier;
5+
6+
fn main() {
7+
// Read the proof, public values, and vkey hash from the input stream.
8+
let proof = sp1_zkvm::io::read_vec();
9+
let sp1_public_values = sp1_zkvm::io::read_vec();
10+
let sp1_vkey_hash: String = sp1_zkvm::io::read();
11+
12+
// Verify the plonk proof.
13+
let groth16_vk = *sp1_verifier::GROTH16_VK_BYTES;
14+
let result =
15+
Groth16Verifier::verify_compressed(&proof, &sp1_public_values, &sp1_vkey_hash, groth16_vk)
16+
.unwrap();
17+
}

crates/verifier/src/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub(crate) const COMPRESSED_INFINITY: u8 = 0b01 << 6;
1010

1111
pub(crate) const VK_HASH_PREFIX_LENGTH: usize = 4;
1212
pub(crate) const GROTH16_PROOF_LENGTH: usize = 256;
13+
pub(crate) const COMPRESSED_GROTH16_PROOF_LENGTH: usize = 128;
1314
pub(crate) const PLONK_CLAIMED_VALUES_COUNT: usize = 5;
1415
pub(crate) const PLONK_CLAIMED_VALUES_OFFSET: usize = 384;
1516
pub(crate) const PLONK_Z_SHIFTED_OPENING_VALUE_OFFSET: usize = 96;

crates/verifier/src/converter.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ use crate::{
77
error::Error,
88
};
99

10+
/// Compresse an G1 point to a buffer.
11+
///
12+
/// This is a reveresed function against `unchecked_compressed_x_to_g1_point`, return the compressed
13+
/// G1 point which hardcoded the sign flag of y coordinate.
14+
pub(crate) fn compress_g1_point_to_x(g1: &AffineG1) -> Result<[u8; 32], Error> {
15+
let mut x_bytes = [0u8; 32];
16+
g1.x().to_big_endian(&mut x_bytes).map_err(Error::Field)?;
17+
18+
if g1.y() > -g1.y() {
19+
x_bytes[0] |= CompressedPointFlag::Negative as u8;
20+
} else {
21+
x_bytes[0] = (x_bytes[0] & !MASK) | (CompressedPointFlag::Positive as u8);
22+
}
23+
24+
Ok(x_bytes)
25+
}
26+
27+
/// Compresse an G2 point to a buffer.
28+
///
29+
/// This is a reveresed function against `unchecked_compressed_x_to_g1_point`, return the compressed
30+
/// G2 point which hardcoded the sign flag of y coordinate.
31+
pub(crate) fn compress_g2_point_to_x(g2: &AffineG2) -> Result<[u8; 64], Error> {
32+
let mut x_bytes = [0u8; 64];
33+
let x1 = Fq::from_u256(g2.x().0.imaginary().0).map_err(Error::Field)?;
34+
let x0 = Fq::from_u256(g2.x().0.real().0).map_err(Error::Field)?;
35+
x1.to_big_endian(&mut x_bytes[..32]).map_err(Error::Field)?;
36+
x0.to_big_endian(&mut x_bytes[32..64]).map_err(Error::Field)?;
37+
38+
if g2.y().0 > -g2.y().0 {
39+
x_bytes[0] |= CompressedPointFlag::Negative as u8;
40+
} else {
41+
x_bytes[0] = (x_bytes[0] & !MASK) | (CompressedPointFlag::Positive as u8);
42+
}
43+
44+
Ok(x_bytes)
45+
}
46+
1047
/// Deserializes an Fq element from a buffer.
1148
///
1249
/// If this Fq element is part of a compressed point, the flag that indicates the sign of the
@@ -70,6 +107,16 @@ pub(crate) fn uncompressed_bytes_to_g1_point(buf: &[u8]) -> Result<AffineG1, Err
70107
AffineG1::new(x, y).map_err(Error::Group)
71108
}
72109

110+
/// Converts an AffineG1 point to an uncompressed byte array.
111+
///
112+
/// The uncompressed byte array is represented as two fq elements.
113+
pub(crate) fn g1_point_to_uncompressed_bytes(point: &AffineG1) -> Result<[u8; 64], Error> {
114+
let mut buffer = [0u8; 64];
115+
point.x().to_big_endian(&mut buffer[..32]).map_err(Error::Field)?;
116+
point.y().to_big_endian(&mut buffer[32..]).map_err(Error::Field)?;
117+
Ok(buffer)
118+
}
119+
73120
/// Converts a compressed G2 point to an AffineG2 point.
74121
///
75122
/// Asserts that the compressed point is represented as a single fq2 element: the x coordinate
@@ -120,3 +167,28 @@ pub(crate) fn uncompressed_bytes_to_g2_point(buf: &[u8]) -> Result<AffineG2, Err
120167

121168
AffineG2::new(x, y).map_err(Error::Group)
122169
}
170+
171+
/// Converts an AffineG2 point to an uncompressed byte array.
172+
///
173+
/// The uncompressed byte array is represented as two fq2 elements.
174+
pub(crate) fn g2_point_to_uncompressed_bytes(point: &AffineG2) -> Result<[u8; 128], Error> {
175+
let mut buffer = [0u8; 128];
176+
Fq::from_u256(point.x().0.imaginary().0)
177+
.unwrap()
178+
.to_big_endian(&mut buffer[..32])
179+
.map_err(Error::Field)?;
180+
Fq::from_u256(point.x().0.real().0)
181+
.unwrap()
182+
.to_big_endian(&mut buffer[32..64])
183+
.map_err(Error::Field)?;
184+
Fq::from_u256(point.y().0.imaginary().0)
185+
.unwrap()
186+
.to_big_endian(&mut buffer[64..96])
187+
.map_err(Error::Field)?;
188+
Fq::from_u256(point.y().0.real().0)
189+
.unwrap()
190+
.to_big_endian(&mut buffer[96..128])
191+
.map_err(Error::Field)?;
192+
193+
Ok(buffer)
194+
}

crates/verifier/src/groth16/converter.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,59 @@
11
use alloc::vec::Vec;
22

33
use crate::{
4-
constants::GROTH16_PROOF_LENGTH,
4+
constants::{COMPRESSED_GROTH16_PROOF_LENGTH, GROTH16_PROOF_LENGTH},
55
converter::{
6-
unchecked_compressed_x_to_g1_point, unchecked_compressed_x_to_g2_point,
7-
uncompressed_bytes_to_g1_point, uncompressed_bytes_to_g2_point,
6+
compress_g1_point_to_x, compress_g2_point_to_x, g1_point_to_uncompressed_bytes,
7+
g2_point_to_uncompressed_bytes, unchecked_compressed_x_to_g1_point,
8+
unchecked_compressed_x_to_g2_point, uncompressed_bytes_to_g1_point,
9+
uncompressed_bytes_to_g2_point,
810
},
911
error::Error,
1012
groth16::{Groth16G1, Groth16G2, Groth16Proof, Groth16VerifyingKey},
1113
};
1214

1315
use super::error::Groth16Error;
1416

17+
/// Compress the Groth16 proof from a byte slice to a compressed byte slice.
18+
///
19+
/// The compressed byte slice is represented as 2 compressed g1 points, and one compressed g2 point,
20+
/// as outputted from Gnark.
21+
pub fn compress_groth16_proof_from_bytes(
22+
buf: &[u8],
23+
) -> Result<[u8; COMPRESSED_GROTH16_PROOF_LENGTH], Groth16Error> {
24+
if buf.len() < GROTH16_PROOF_LENGTH {
25+
return Err(Groth16Error::GeneralError(Error::InvalidData));
26+
}
27+
28+
let proof = load_groth16_proof_from_bytes(buf)?;
29+
let mut buffer = [0u8; COMPRESSED_GROTH16_PROOF_LENGTH];
30+
buffer[..32].copy_from_slice(&compress_g1_point_to_x(&proof.ar)?);
31+
buffer[32..96].copy_from_slice(&compress_g2_point_to_x(&proof.bs)?);
32+
buffer[96..128].copy_from_slice(&compress_g1_point_to_x(&proof.krs)?);
33+
34+
Ok(buffer)
35+
}
36+
37+
/// Decompress the Groth16 proof from a compressed byte slice to a byte slice.
38+
///
39+
/// The byte slice is represented as 2 uncompressed g1 points, and one uncompressed g2 point,
40+
/// as outputted from Gnark.
41+
pub fn decompress_groth16_proof_from_bytes(
42+
buf: &[u8],
43+
) -> Result<[u8; GROTH16_PROOF_LENGTH], Groth16Error> {
44+
if buf.len() < COMPRESSED_GROTH16_PROOF_LENGTH {
45+
return Err(Groth16Error::GeneralError(Error::InvalidData));
46+
}
47+
48+
let proof = load_compressed_groth16_proof_from_bytes(buf)?;
49+
let mut buffer = [0u8; GROTH16_PROOF_LENGTH];
50+
buffer[..64].copy_from_slice(&g1_point_to_uncompressed_bytes(&proof.ar)?);
51+
buffer[64..192].copy_from_slice(&g2_point_to_uncompressed_bytes(&proof.bs)?);
52+
buffer[192..256].copy_from_slice(&g1_point_to_uncompressed_bytes(&proof.krs)?);
53+
54+
Ok(buffer)
55+
}
56+
1557
/// Load the Groth16 proof from the given byte slice.
1658
///
1759
/// The byte slice is represented as 2 uncompressed g1 points, and one uncompressed g2 point,
@@ -36,7 +78,7 @@ pub(crate) fn load_groth16_proof_from_bytes(buffer: &[u8]) -> Result<Groth16Proo
3678
pub(crate) fn load_compressed_groth16_proof_from_bytes(
3779
buffer: &[u8],
3880
) -> Result<Groth16Proof, Groth16Error> {
39-
if buffer.len() < GROTH16_PROOF_LENGTH / 2 {
81+
if buffer.len() < COMPRESSED_GROTH16_PROOF_LENGTH {
4082
return Err(Groth16Error::GeneralError(Error::InvalidData));
4183
}
4284
let (ar, bs, krs) = (

crates/verifier/src/groth16/mod.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mod converter;
1+
pub mod converter;
22
pub mod error;
33
mod verify;
44

@@ -126,6 +126,72 @@ impl Groth16Verifier {
126126
verify_groth16_algebraic(&groth16_vk, &proof, &public_inputs)
127127
}
128128

129+
/// Verifies an SP1 compressed Groth16 proof, as generated by the SP1 SDK.
130+
///
131+
/// # Arguments
132+
///
133+
/// * `proof` - The proof bytes.
134+
/// * `public_inputs` - The SP1 public inputs.
135+
/// * `sp1_vkey_hash` - The SP1 vkey hash. This is generated in the following manner:
136+
///
137+
/// ```ignore
138+
/// use sp1_sdk::ProverClient;
139+
/// let client = ProverClient::new();
140+
/// let (pk, vk) = client.setup(ELF);
141+
/// let sp1_vkey_hash = vk.bytes32();
142+
/// ```
143+
/// * `groth16_vk` - The Groth16 verifying key bytes. Usually this will be the
144+
/// [`static@crate::GROTH16_VK_BYTES`] constant, which is the Groth16 verifying key for the
145+
/// current SP1 version.
146+
///
147+
/// # Returns
148+
///
149+
/// A success [`Result`] if verification succeeds, or a [`Groth16Error`] if verification fails.
150+
pub fn verify_compressed(
151+
proof: &[u8],
152+
sp1_public_inputs: &[u8],
153+
sp1_vkey_hash: &str,
154+
groth16_vk: &[u8],
155+
) -> Result<(), Groth16Error> {
156+
if proof.len() < VK_HASH_PREFIX_LENGTH {
157+
return Err(Groth16Error::GeneralError(Error::InvalidData));
158+
}
159+
160+
// Hash the vk and get the first 4 bytes.
161+
let groth16_vk_hash: [u8; 4] = Sha256::digest(groth16_vk)[..VK_HASH_PREFIX_LENGTH]
162+
.try_into()
163+
.map_err(|_| Groth16Error::GeneralError(Error::InvalidData))?;
164+
165+
// Check to make sure that this proof was generated by the groth16 proving key corresponding
166+
// to the given groth16_vk.
167+
//
168+
// SP1 prepends the raw Groth16 proof with the first 4 bytes of the groth16 vkey to
169+
// facilitate this check.
170+
if groth16_vk_hash != proof[..VK_HASH_PREFIX_LENGTH] {
171+
return Err(Groth16Error::Groth16VkeyHashMismatch);
172+
}
173+
174+
let sp1_vkey_hash = decode_sp1_vkey_hash(sp1_vkey_hash)?;
175+
176+
// It is computationally infeasible to find two distinct inputs, one processed with
177+
// SHA256 and the other with Blake3, that yield the same hash value.
178+
if Self::verify_compressed_gnark_proof(
179+
&proof[VK_HASH_PREFIX_LENGTH..],
180+
&[sp1_vkey_hash, hash_public_inputs(sp1_public_inputs)],
181+
groth16_vk,
182+
)
183+
.is_ok()
184+
{
185+
return Ok(());
186+
}
187+
188+
Self::verify_compressed_gnark_proof(
189+
&proof[VK_HASH_PREFIX_LENGTH..],
190+
&[sp1_vkey_hash, hash_public_inputs_with_fn(sp1_public_inputs, blake3_hash)],
191+
groth16_vk,
192+
)
193+
}
194+
129195
/// Verifies a compressed Gnark Groth16 proof using raw byte inputs.
130196
///
131197
/// WARNING: if you're verifying an SP1 proof, you should use [`verify`] instead.

crates/verifier/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ mod error;
2828
mod utils;
2929
pub use utils::*;
3030

31-
pub use groth16::{error::Groth16Error, Groth16Verifier};
31+
pub use groth16::{converter::*, error::Groth16Error, Groth16Verifier};
3232
mod groth16;
3333

3434
#[cfg(feature = "ark")]

0 commit comments

Comments
 (0)