Skip to main content

taceo_groth16_sol/
lib.rs

1//! # Groth16 Solidity generator
2//!
3//! A crate for generating Solidity verifier contracts for BN254 Groth16 proofs.
4//! This crate uses the `askama` templating engine to render Solidity code based on
5//! the provided verifying key and configuration options.
6//!
7//! The solidity contract is based on the [Groth16 verifier implementation from
8//! gnark](https://github.com/Consensys/gnark/blob/9c9cf0deb462ea302af36872669457c36da0f160/backend/groth16/bn254/solidity.go),
9//! with minor modifications to be compatible with the [askama](docs.rs/askama) crate.
10//!
11//! ## Example usage
12//! Generation of the Solidity verifier contract can be done as follows and requires the `template` feature to be enabled, which it is by default.
13//! If the features is enabled, the crate also re-exports `askama` for convenience.
14//! ```rust,no_run
15//! # #[cfg(feature = "template")]
16//! # {
17//! # fn load_verification_key() -> ark_groth16::VerifyingKey<ark_bn254::Bn254> { todo!() }
18//! use taceo_groth16_sol::{SolidityVerifierConfig, SolidityVerifierContext};
19//! use taceo_groth16_sol::askama::Template;
20//! let config = SolidityVerifierConfig::default();
21//! let vk : ark_groth16::VerifyingKey<ark_bn254::Bn254> = load_verification_key();
22//! let contract = SolidityVerifierContext {
23//!     vk,
24//!     config,
25//! };
26//! let rendered = contract.render().unwrap();
27//! println!("{}", rendered);
28//! // You can also write the rendered contract to a file, see askama documentation for details
29//! let mut file = std::fs::File::create("Verifier.sol").unwrap();
30//! contract.write_into(&mut file).unwrap();
31//! # }
32//! ```
33//! ## Preparing proofs
34//! The crate also provides utility functions to prepare Groth16 proofs for verification in the generated contract.
35//! The proofs can be prepared in either compressed or uncompressed format, depending on the specific deployment of the verifier contract.
36//! See <https://2π.com/23/bn254-compression> for explanation of the point compression scheme used and explanation of the gas tradeoffs.
37//! ```rust,no_run
38//! # fn load_proof() -> ark_groth16::Proof<ark_bn254::Bn254> { todo!() }
39//! let proof: ark_groth16::Proof<ark_bn254::Bn254> = load_proof();
40//! let compressed_proof = taceo_groth16_sol::prepare_compressed_proof(&proof);
41//! let uncompressed_proof = taceo_groth16_sol::prepare_uncompressed_proof(&proof);
42//! ```
43#![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/// Re-export askama for users of this crate
52#[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    /// Context for generating a Solidity verifier contract for BN254 Groth16 proofs.
64    /// The context is passed to `askama` for template rendering.
65    /// Parameters:
66    /// - `vk`: The [verifying key](ark_groth16::VerifyingKey) for the BN254 curve.
67    /// - `config`: Configuration options for the Solidity verifier contract generation.
68    #[derive(Debug, Clone, Template)]
69    #[template(path = "../templates/bn254_verifier.sol", escape = "none")]
70    pub struct SolidityVerifierContext {
71        /// The Groth16 verifying key
72        pub vk: VerifyingKey<ark_bn254::Bn254>,
73        /// Configuration options for the Solidity verifier contract generation
74        pub config: SolidityVerifierConfig,
75    }
76
77    /// Configuration for the Solidity verifier contract generation.
78    ///
79    /// Parameters:
80    /// - `pragma_version`: The Solidity pragma version to use in the generated contract. Default is "^0.8.0".
81    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
82    pub struct SolidityVerifierConfig {
83        /// The Solidity pragma version to use in the generated contract. Default is "^0.8.0".
84        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            // Askama supresses trailing newlines, so we add one for comparison
115            let rendered = format!("{}\n", rendered);
116            assert_eq!(rendered, TEST_GNARK_OUTPUT);
117        }
118    }
119}
120
121/// Compress a G1 point into a single U256, using the method described in the contract.
122/// See <https://2π.com/23/bn254-compression> for further explanation.
123fn 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, // Infinity represented as 0
139    }
140}
141
142/// Compress a G2 point into two U256s, using the method described in the contract.
143/// See <https://2π.com/23/bn254-compression> for further explanation.
144fn 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], // Infinity represented as (0, 0)
186    }
187}
188
189/// Compress a Groth16 proofs by compressing the individual curve points.
190/// This method uses the point compression scheme described in the contract.
191/// See <https://2π.com/23/bn254-compression> for further explanation.
192///
193/// # Panics
194///
195/// This function will panic if the proof contains points that are not on the respective curves.
196pub 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
204/// Prepare an uncompressed Groth16 proof for verification in the generated contract.
205/// The proof is represented as an array of 8 U256 values, corresponding to the
206/// x and y coordinates of the points A, B, and C in the proof.
207pub fn prepare_uncompressed_proof(proof: &Proof<ark_bn254::Bn254>) -> [U256; 8] {
208    // Infinity is represented as (0, 0)
209    let (ax, ay) = proof.a.xy().unwrap_or_default();
210    // Infinity is represented as (0, 0, 0, 0)
211    let (bx, by) = proof.b.xy().unwrap_or_default();
212    // Infinity is represented as (0, 0)
213    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/// An error type representing an invalid compressed point during decompression.
228#[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        // Infinity point
242        return Ok(G1Affine::identity());
243    }
244    let x: U256 = compressed >> 1;
245    // Checks that x is a valid field element < p
246    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 is the negative square root
251        -y
252    } else {
253        // y is the positive square root
254        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
264/// Decompress a G2 point from its compressed representation.
265/// The input array is [c0, c1], with c0 being the low degree term of the quadratic extension field..
266fn 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        // Infinity point
271        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    // Checks that x0 is a valid field element < p
278    let x0 = Fq::try_from(x0).map_err(|_| InvalidCompressedPoint)?;
279    // Checks that x1 is a valid field element < p
280    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    // recompute the hint and check it against the compressed value to ensure the point is valid
295    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
311/// Decompress a Groth16 proof from its compressed representation.
312///
313/// The input is an array of 4 U256 values, corresponding to the compressed points A, B, and C in the proof.
314/// The output is a `Proof<ark_bn254::Bn254>` that can be used for verification with arkworks.
315///
316/// # Errors
317/// This function will return an error if any of the compressed points are invalid.
318pub 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        // Test G1 point compression and decompression
345        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        // Test G2 point compression and decompression
359        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}