1#![deny(missing_docs)]
44
45use alloy_primitives::U256;
46use ark_bn254::{Fq, G1Affine, G2Affine};
47use ark_ec::AffineRepr;
48use ark_ff::Field;
49use ark_groth16::Proof;
50
51#[cfg(feature = "template")]
53pub use askama;
54#[cfg(feature = "template")]
55pub use template::{SolidityVerifierConfig, SolidityVerifierContext};
56
57#[cfg(feature = "template")]
58mod template {
59 use ark_ec::AffineRepr;
60 use ark_groth16::VerifyingKey;
61 use askama::Template;
62
63 #[derive(Debug, Clone, Template)]
69 #[template(path = "../templates/bn254_verifier.sol", escape = "none")]
70 pub struct SolidityVerifierContext {
71 pub vk: VerifyingKey<ark_bn254::Bn254>,
73 pub config: SolidityVerifierConfig,
75 }
76
77 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
82 pub struct SolidityVerifierConfig {
83 pub pragma_version: String,
85 }
86
87 impl Default for SolidityVerifierConfig {
88 fn default() -> Self {
89 Self {
90 pragma_version: "^0.8.0".to_string(),
91 }
92 }
93 }
94
95 #[cfg(test)]
96 mod tests {
97 use askama::Template;
98 use circom_types::groth16::VerificationKey;
99
100 const TEST_VK_BN254: &str = include_str!("../data/test_verification_key.json");
101 const TEST_GNARK_OUTPUT: &str = include_str!("../data/gnark_output.txt");
102
103 #[test]
104 fn test() {
105 let config = super::SolidityVerifierConfig::default();
106 let vk =
107 serde_json::from_str::<VerificationKey<ark_bn254::Bn254>>(TEST_VK_BN254).unwrap();
108 let contract = super::SolidityVerifierContext {
109 vk: vk.into(),
110 config,
111 };
112
113 let rendered = contract.render().unwrap();
114 let rendered = format!("{}\n", rendered);
116 assert_eq!(rendered, TEST_GNARK_OUTPUT);
117 }
118 }
119}
120
121fn compress_g1_point(point: &G1Affine) -> U256 {
124 match point.xy() {
125 Some((x, y)) => {
126 let x_comp: U256 = x.into();
127 let y_sqr = x.pow([3]) + ark_bn254::Fq::from(3);
128 let y_computed = y_sqr
129 .sqrt()
130 .expect("Point is not on curve, this should not happen");
131 if y == y_computed {
132 x_comp << 1
133 } else {
134 assert_eq!(y, -y_computed);
135 (x_comp << 1) | U256::ONE
136 }
137 }
138 None => U256::ZERO, }
140}
141
142fn compress_g2_point(point: &G2Affine) -> [U256; 2] {
145 match point.xy() {
146 Some((x, y)) => {
147 let n3ab = x.c0 * x.c1 * Fq::from(-3);
148 let a_3 = x.c0.pow([3]);
149 let b_3 = x.c1.pow([3]);
150
151 let frac_27_82 = Fq::from(27) * Fq::from(82).inverse().unwrap();
152 let frac_3_82 = Fq::from(3) * Fq::from(82).inverse().unwrap();
153 let y0_pos = (n3ab * x.c1) + a_3 + frac_27_82;
154 let y1_pos = -((n3ab * x.c0) + b_3 + frac_3_82);
155
156 let half = Fq::from(2).inverse().unwrap();
157 let d = ((y0_pos * y0_pos) + (y1_pos * y1_pos))
158 .sqrt()
159 .expect("x is not on curve, this should not happen");
160 let hint = ((y0_pos + d) * half).sqrt().is_none();
161
162 let y2 = ark_bn254::Fq2::new(y0_pos, y1_pos);
163 let y_computed = y2
164 .sqrt()
165 .expect("Point is on curve, this should not happen");
166 if y_computed == y {
167 let b0_comp: U256 = x.c0.into();
168 let b1_comp: U256 = x.c1.into();
169 if hint {
170 [b0_comp << 2 | U256::ONE << 1, b1_comp]
171 } else {
172 [b0_comp << 2, b1_comp]
173 }
174 } else {
175 assert_eq!(y, -y_computed);
176 let b0_comp: U256 = x.c0.into();
177 let b1_comp: U256 = x.c1.into();
178 if hint {
179 [b0_comp << 2 | (U256::ONE << 1) | U256::ONE, b1_comp]
180 } else {
181 [b0_comp << 2 | U256::ONE, b1_comp]
182 }
183 }
184 }
185 None => [U256::ZERO, U256::ZERO], }
187}
188
189pub fn prepare_compressed_proof(proof: &Proof<ark_bn254::Bn254>) -> [U256; 4] {
197 let a_compressed = compress_g1_point(&proof.a);
198 let [b0_compressed, b1_compressed] = compress_g2_point(&proof.b);
199 let c_compressed = compress_g1_point(&proof.c);
200
201 [a_compressed, b1_compressed, b0_compressed, c_compressed]
202}
203
204pub fn prepare_uncompressed_proof(proof: &Proof<ark_bn254::Bn254>) -> [U256; 8] {
208 let (ax, ay) = proof.a.xy().unwrap_or_default();
210 let (bx, by) = proof.b.xy().unwrap_or_default();
212 let (cx, cy) = proof.c.xy().unwrap_or_default();
214
215 [
216 ax.into(),
217 ay.into(),
218 bx.c1.into(),
219 bx.c0.into(),
220 by.c1.into(),
221 by.c0.into(),
222 cx.into(),
223 cy.into(),
224 ]
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub struct InvalidCompressedPoint;
230
231impl core::error::Error for InvalidCompressedPoint {}
232
233impl core::fmt::Display for InvalidCompressedPoint {
234 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
235 write!(f, "Encountered an invalid point during decompression")
236 }
237}
238
239fn decompress_g1_point(compressed: U256) -> Result<G1Affine, InvalidCompressedPoint> {
240 if compressed.is_zero() {
241 return Ok(G1Affine::identity());
243 }
244 let x: U256 = compressed >> 1;
245 let x = Fq::try_from(x).map_err(|_| InvalidCompressedPoint)?;
247 let y_sqr = x.pow([3]) + Fq::from(3);
248 let y = y_sqr.sqrt().ok_or(InvalidCompressedPoint)?;
249 let y = if compressed.bit(0) {
250 -y
252 } else {
253 y
255 };
256 let res = G1Affine::new_unchecked(x, y);
257 if res.is_on_curve() && res.is_in_correct_subgroup_assuming_on_curve() {
258 Ok(res)
259 } else {
260 Err(InvalidCompressedPoint)
261 }
262}
263
264fn decompress_g2_point(compressed: [U256; 2]) -> Result<G2Affine, InvalidCompressedPoint> {
267 let c0 = compressed[0];
268 let c1 = compressed[1];
269 if c0.is_zero() && c1.is_zero() {
270 return Ok(G2Affine::identity());
272 }
273 let negate_point = c0.bit(0);
274 let hint = c0.bit(1);
275 let x0: U256 = c0 >> 2;
276 let x1 = c1;
277 let x0 = Fq::try_from(x0).map_err(|_| InvalidCompressedPoint)?;
279 let x1 = Fq::try_from(x1).map_err(|_| InvalidCompressedPoint)?;
281
282 let n3ab = x0 * x1 * Fq::from(-3);
283 let a_3 = x0.pow([3]);
284 let b_3 = x1.pow([3]);
285
286 let frac_27_82 = Fq::from(27) * Fq::from(82).inverse().unwrap();
287 let frac_3_82 = Fq::from(3) * Fq::from(82).inverse().unwrap();
288 let y0_pos = (n3ab * x1) + a_3 + frac_27_82;
289 let y1_pos = -((n3ab * x0) + b_3 + frac_3_82);
290
291 let y2 = ark_bn254::Fq2::new(y0_pos, y1_pos);
292 let y = y2.sqrt().ok_or(InvalidCompressedPoint)?;
293 let y = if negate_point { -y } else { y };
294 let half = Fq::from(2).inverse().unwrap();
296 let d = ((y0_pos * y0_pos) + (y1_pos * y1_pos))
297 .sqrt()
298 .ok_or(InvalidCompressedPoint)?;
299 let hint_recomputed = ((y0_pos + d) * half).sqrt().is_none();
300 if hint != hint_recomputed {
301 return Err(InvalidCompressedPoint);
302 }
303 let res = G2Affine::new_unchecked(ark_bn254::Fq2::new(x0, x1), y);
304 if res.is_on_curve() && res.is_in_correct_subgroup_assuming_on_curve() {
305 Ok(res)
306 } else {
307 Err(InvalidCompressedPoint)
308 }
309}
310
311pub fn decompress_proof(
319 compressed_proof: &[U256; 4],
320) -> Result<Proof<ark_bn254::Bn254>, InvalidCompressedPoint> {
321 let a_compressed = compressed_proof[0];
322 let b1_compressed = compressed_proof[1];
323 let b0_compressed = compressed_proof[2];
324 let c_compressed = compressed_proof[3];
325
326 let a = decompress_g1_point(a_compressed)?;
327 let b = decompress_g2_point([b0_compressed, b1_compressed])?;
328 let c = decompress_g1_point(c_compressed)?;
329
330 Ok(Proof { a, b, c })
331}
332
333#[cfg(test)]
334mod tests {
335 use ark_ff::UniformRand;
336
337 #[test]
338 fn test_roundtrip_compression() {
339 use ark_bn254::{G1Affine, G2Affine};
340 use rand::thread_rng;
341
342 let mut rng = thread_rng();
343
344 for _ in 0..100 {
346 let point = G1Affine::rand(&mut rng);
347 let compressed = super::compress_g1_point(&point);
348 let decompressed = super::decompress_g1_point(compressed).unwrap();
349 assert_eq!(point, decompressed);
350 }
351 {
352 let point = G1Affine::identity();
353 let compressed = super::compress_g1_point(&point);
354 let decompressed = super::decompress_g1_point(compressed).unwrap();
355 assert_eq!(point, decompressed);
356 }
357
358 for _ in 0..100 {
360 let point = G2Affine::rand(&mut rng);
361 let compressed = super::compress_g2_point(&point);
362 let decompressed = super::decompress_g2_point(compressed).unwrap();
363 assert_eq!(point, decompressed);
364 }
365 {
366 let point = G2Affine::identity();
367 let compressed = super::compress_g2_point(&point);
368 let decompressed = super::decompress_g2_point(compressed).unwrap();
369 assert_eq!(point, decompressed);
370 }
371 }
372}