Skip to main content

light_verifier/
lib.rs

1//! # light-verifier
2//!
3//! ZK proof verifier for Light Protocol. Verifies Groth16 proofs
4//! for inclusion, non-inclusion, and combined address+state operations.
5//!
6//! | Function | Description |
7//! |----------|-------------|
8//! | [`verify_inclusion_proof`] | Verify inclusion for 1–8+ leaves |
9//! | [`verify_create_addresses_proof`] | Verify non-inclusion for 1–8 addresses |
10//! | [`verify_create_addresses_and_inclusion_proof`] | Verify combined address and state proof |
11//! | [`verify_batch_append_with_proofs`] | Verify batch append (10 or 500 leaves) |
12//! | [`verify_batch_update`] | Verify batch state update (10 or 500) |
13//! | [`verify_batch_address_update`] | Verify batch address update (10 or 250) |
14//! | [`select_verifying_key`] | Route to correct verifying key by leaf/address count |
15//! | [`verify`] | Generic Groth16 proof verification |
16
17use groth16_solana::{
18    decompression::{decompress_g1, decompress_g2},
19    groth16::{Groth16Verifier, Groth16Verifyingkey},
20};
21use thiserror::Error;
22
23use crate::verifying_keys::*;
24
25pub mod verifying_keys;
26#[derive(Debug, Error, PartialEq)]
27pub enum VerifierError {
28    #[error("PublicInputsTryIntoFailed")]
29    PublicInputsTryIntoFailed,
30    #[error("DecompressG1Failed")]
31    DecompressG1Failed,
32    #[error("DecompressG2Failed")]
33    DecompressG2Failed,
34    #[error("InvalidPublicInputsLength")]
35    InvalidPublicInputsLength,
36    #[error("CreateGroth16VerifierFailed")]
37    CreateGroth16VerifierFailed,
38    #[error("ProofVerificationFailed")]
39    ProofVerificationFailed,
40    #[error("InvalidBatchSize supported batch sizes are 1, 10, 100, 500, 1000")]
41    InvalidBatchSize,
42}
43
44impl From<VerifierError> for u32 {
45    fn from(e: VerifierError) -> u32 {
46        match e {
47            PublicInputsTryIntoFailed => 13001,
48            DecompressG1Failed => 13002,
49            DecompressG2Failed => 13003,
50            InvalidPublicInputsLength => 13004,
51            CreateGroth16VerifierFailed => 13005,
52            ProofVerificationFailed => 13006,
53            InvalidBatchSize => 13007,
54        }
55    }
56}
57
58#[cfg(feature = "solana")]
59impl From<VerifierError> for solana_program_error::ProgramError {
60    fn from(e: VerifierError) -> Self {
61        solana_program_error::ProgramError::Custom(e.into())
62    }
63}
64
65#[cfg(feature = "pinocchio")]
66impl From<VerifierError> for pinocchio::program_error::ProgramError {
67    fn from(e: VerifierError) -> Self {
68        pinocchio::program_error::ProgramError::Custom(e.into())
69    }
70}
71
72pub use light_compressed_account::instruction_data::compressed_proof::CompressedProof;
73use VerifierError::*;
74
75pub fn verify_create_addresses_proof(
76    address_roots: &[[u8; 32]],
77    addresses: &[[u8; 32]],
78    compressed_proof: &CompressedProof,
79) -> Result<(), VerifierError> {
80    let public_inputs = [address_roots, addresses].concat();
81    match addresses.len() {
82        1 => verify::<2>(
83            &public_inputs
84                .try_into()
85                .map_err(|_| PublicInputsTryIntoFailed)?,
86            compressed_proof,
87            &v1_non_inclusion_26_1::VERIFYINGKEY,
88        ),
89        2 => verify::<4>(
90            &public_inputs
91                .try_into()
92                .map_err(|_| PublicInputsTryIntoFailed)?,
93            compressed_proof,
94            &v1_non_inclusion_26_2::VERIFYINGKEY,
95        ),
96        3 => verify::<6>(
97            &public_inputs
98                .try_into()
99                .map_err(|_| PublicInputsTryIntoFailed)?,
100            compressed_proof,
101            &v1_non_inclusion_26_3::VERIFYINGKEY,
102        ),
103        4 => verify::<8>(
104            &public_inputs
105                .try_into()
106                .map_err(|_| PublicInputsTryIntoFailed)?,
107            compressed_proof,
108            &v1_non_inclusion_26_4::VERIFYINGKEY,
109        ),
110        8 => verify::<16>(
111            &public_inputs
112                .try_into()
113                .map_err(|_| PublicInputsTryIntoFailed)?,
114            compressed_proof,
115            &v1_non_inclusion_26_8::VERIFYINGKEY,
116        ),
117        _ => Err(InvalidPublicInputsLength),
118    }
119}
120
121#[inline(never)]
122pub fn verify_create_addresses_and_inclusion_proof(
123    roots: &[[u8; 32]],
124    leaves: &[[u8; 32]],
125    address_roots: &[[u8; 32]],
126    addresses: &[[u8; 32]],
127    compressed_proof: &CompressedProof,
128) -> Result<(), VerifierError> {
129    let public_inputs = [roots, leaves, address_roots, addresses].concat();
130    // The public inputs are expected to be a multiple of 2
131    // 4 inputs means 1 inclusion proof (1 root, 1 leaf, 1 address root, 1 created address)
132    // 6 inputs means 1 inclusion proof (1 root, 1 leaf, 2 address roots, 2 created address) or
133    // 6 inputs means 2 inclusion proofs (2 roots and 2 leaves, 1 address root, 1 created address)
134    // 8 inputs means 2 inclusion proofs (2 roots and 2 leaves, 2 address roots, 2 created address) or
135    // 8 inputs means 3 inclusion proofs (3 roots and 3 leaves, 1 address root, 1 created address)
136    // 10 inputs means 3 inclusion proofs (3 roots and 3 leaves, 2 address roots, 2 created address) or
137    // 10 inputs means 4 inclusion proofs (4 roots and 4 leaves, 1 address root, 1 created address)
138    // 12 inputs means 4 inclusion proofs (4 roots and 4 leaves, 2 address roots, 2 created address)
139    match public_inputs.len() {
140        4 => verify::<4>(
141            &public_inputs
142                .try_into()
143                .map_err(|_| PublicInputsTryIntoFailed)?,
144            compressed_proof,
145            &v1_combined_26_26_1_1::VERIFYINGKEY,
146        ),
147        6 => {
148            let verifying_key = if address_roots.len() == 1 {
149                &v1_combined_26_26_2_1::VERIFYINGKEY
150            } else {
151                &v1_combined_26_26_1_2::VERIFYINGKEY
152            };
153            verify::<6>(
154                &public_inputs
155                    .try_into()
156                    .map_err(|_| PublicInputsTryIntoFailed)?,
157                compressed_proof,
158                verifying_key,
159            )
160        }
161        8 => {
162            let verifying_key = if address_roots.len() == 1 {
163                &v1_combined_26_26_3_1::VERIFYINGKEY
164            } else {
165                &v1_combined_26_26_2_2::VERIFYINGKEY
166            };
167            verify::<8>(
168                &public_inputs
169                    .try_into()
170                    .map_err(|_| PublicInputsTryIntoFailed)?,
171                compressed_proof,
172                verifying_key,
173            )
174        }
175        10 => {
176            let verifying_key = if address_roots.len() == 1 {
177                &v1_combined_26_26_4_1::VERIFYINGKEY
178            } else {
179                &v1_combined_26_26_3_2::VERIFYINGKEY
180            };
181            verify::<10>(
182                &public_inputs
183                    .try_into()
184                    .map_err(|_| PublicInputsTryIntoFailed)?,
185                compressed_proof,
186                verifying_key,
187            )
188        }
189        12 => verify::<12>(
190            &public_inputs
191                .try_into()
192                .map_err(|_| PublicInputsTryIntoFailed)?,
193            compressed_proof,
194            &v1_combined_26_26_4_2::VERIFYINGKEY,
195        ),
196        _ => Err(InvalidPublicInputsLength),
197    }
198}
199
200#[inline(never)]
201pub fn verify_inclusion_proof(
202    roots: &[[u8; 32]],
203    leaves: &[[u8; 32]],
204    compressed_proof: &CompressedProof,
205) -> Result<(), VerifierError> {
206    let public_inputs = [roots, leaves].concat();
207
208    // The public inputs are expected to be a multiple of 2
209    // 2 inputs means 1 inclusion proof (1 root and 1 leaf)
210    // 4 inputs means 2 inclusion proofs (2 roots and 2 leaves)
211    // 6 inputs means 3 inclusion proofs (3 roots and 3 leaves)
212    // 8 inputs means 4 inclusion proofs (4 roots and 4 leaves)
213    // 16 inputs means 8 inclusion proofs (8 roots and 8 leaves)
214    match public_inputs.len() {
215        2 => verify::<2>(
216            &public_inputs
217                .try_into()
218                .map_err(|_| PublicInputsTryIntoFailed)?,
219            compressed_proof,
220            &v1_inclusion_26_1::VERIFYINGKEY,
221        ),
222        4 => verify::<4>(
223            &public_inputs
224                .try_into()
225                .map_err(|_| PublicInputsTryIntoFailed)?,
226            compressed_proof,
227            &v1_inclusion_26_2::VERIFYINGKEY,
228        ),
229        6 => verify::<6>(
230            &public_inputs
231                .try_into()
232                .map_err(|_| PublicInputsTryIntoFailed)?,
233            compressed_proof,
234            &v1_inclusion_26_3::VERIFYINGKEY,
235        ),
236        8 => verify::<8>(
237            &public_inputs
238                .try_into()
239                .map_err(|_| PublicInputsTryIntoFailed)?,
240            compressed_proof,
241            &v1_inclusion_26_4::VERIFYINGKEY,
242        ),
243        16 => verify::<16>(
244            &public_inputs
245                .try_into()
246                .map_err(|_| PublicInputsTryIntoFailed)?,
247            compressed_proof,
248            &v1_inclusion_26_8::VERIFYINGKEY,
249        ),
250        _ => Err(InvalidPublicInputsLength),
251    }
252}
253
254pub fn select_verifying_key<'a>(
255    num_leaves: usize,
256    num_addresses: usize,
257) -> Result<&'a Groth16Verifyingkey<'static>, VerifierError> {
258    #[cfg(all(feature = "solana", target_os = "solana"))]
259    solana_msg::msg!(
260        "select_verifying_key num_leaves: {}, num_addresses: {}",
261        num_leaves,
262        num_addresses
263    );
264    match (num_leaves, num_addresses) {
265        // Combined cases (depend on both num_leaves and num_addresses)
266        (1, 1) => Ok(&v2_combined_32_40_1_1::VERIFYINGKEY),
267        (1, 2) => Ok(&v2_combined_32_40_1_2::VERIFYINGKEY),
268        (1, 3) => Ok(&v2_combined_32_40_1_3::VERIFYINGKEY),
269        (1, 4) => Ok(&v2_combined_32_40_1_4::VERIFYINGKEY),
270        (2, 1) => Ok(&v2_combined_32_40_2_1::VERIFYINGKEY),
271        (2, 2) => Ok(&v2_combined_32_40_2_2::VERIFYINGKEY),
272        (2, 3) => Ok(&v2_combined_32_40_2_3::VERIFYINGKEY),
273        (2, 4) => Ok(&v2_combined_32_40_2_4::VERIFYINGKEY),
274        (3, 1) => Ok(&v2_combined_32_40_3_1::VERIFYINGKEY),
275        (3, 2) => Ok(&v2_combined_32_40_3_2::VERIFYINGKEY),
276        (3, 3) => Ok(&v2_combined_32_40_3_3::VERIFYINGKEY),
277        (3, 4) => Ok(&v2_combined_32_40_3_4::VERIFYINGKEY),
278        (4, 1) => Ok(&v2_combined_32_40_4_1::VERIFYINGKEY),
279        (4, 2) => Ok(&v2_combined_32_40_4_2::VERIFYINGKEY),
280        (4, 3) => Ok(&v2_combined_32_40_4_3::VERIFYINGKEY),
281        (4, 4) => Ok(&v2_combined_32_40_4_4::VERIFYINGKEY),
282
283        // Inclusion cases (depend on num_leaves)
284        (1, _) => Ok(&v2_inclusion_32_1::VERIFYINGKEY),
285        (2, _) => Ok(&v2_inclusion_32_2::VERIFYINGKEY),
286        (3, _) => Ok(&v2_inclusion_32_3::VERIFYINGKEY),
287        (4, _) => Ok(&v2_inclusion_32_4::VERIFYINGKEY),
288        (5, _) => Ok(&v2_inclusion_32_5::VERIFYINGKEY),
289        (6, _) => Ok(&v2_inclusion_32_6::VERIFYINGKEY),
290        (7, _) => Ok(&v2_inclusion_32_7::VERIFYINGKEY),
291        (8, _) => Ok(&v2_inclusion_32_8::VERIFYINGKEY),
292        (9, _) => Ok(&v2_inclusion_32_9::VERIFYINGKEY),
293        (10, _) => Ok(&v2_inclusion_32_10::VERIFYINGKEY),
294        (11, _) => Ok(&v2_inclusion_32_11::VERIFYINGKEY),
295        (12, _) => Ok(&v2_inclusion_32_12::VERIFYINGKEY),
296        (13, _) => Ok(&v2_inclusion_32_13::VERIFYINGKEY),
297        (14, _) => Ok(&v2_inclusion_32_14::VERIFYINGKEY),
298        (15, _) => Ok(&v2_inclusion_32_15::VERIFYINGKEY),
299        (16, _) => Ok(&v2_inclusion_32_16::VERIFYINGKEY),
300        (17, _) => Ok(&v2_inclusion_32_17::VERIFYINGKEY),
301        (18, _) => Ok(&v2_inclusion_32_18::VERIFYINGKEY),
302        (19, _) => Ok(&v2_inclusion_32_19::VERIFYINGKEY),
303        (20, _) => Ok(&v2_inclusion_32_20::VERIFYINGKEY),
304
305        // Non-inclusion cases (depend on num_addresses)
306        (_, 1) => Ok(&v2_non_inclusion_40_1::VERIFYINGKEY),
307        (_, 2) => Ok(&v2_non_inclusion_40_2::VERIFYINGKEY),
308        (_, 3) => Ok(&v2_non_inclusion_40_3::VERIFYINGKEY),
309        (_, 4) => Ok(&v2_non_inclusion_40_4::VERIFYINGKEY),
310        (_, 5) => Ok(&v2_non_inclusion_40_5::VERIFYINGKEY),
311        (_, 6) => Ok(&v2_non_inclusion_40_6::VERIFYINGKEY),
312        (_, 7) => Ok(&v2_non_inclusion_40_7::VERIFYINGKEY),
313        (_, 8) => Ok(&v2_non_inclusion_40_8::VERIFYINGKEY),
314        (_, 9) => Ok(&v2_non_inclusion_40_9::VERIFYINGKEY),
315        (_, 10) => Ok(&v2_non_inclusion_40_10::VERIFYINGKEY),
316        (_, 11) => Ok(&v2_non_inclusion_40_11::VERIFYINGKEY),
317        (_, 12) => Ok(&v2_non_inclusion_40_12::VERIFYINGKEY),
318        (_, 13) => Ok(&v2_non_inclusion_40_13::VERIFYINGKEY),
319        (_, 14) => Ok(&v2_non_inclusion_40_14::VERIFYINGKEY),
320        (_, 15) => Ok(&v2_non_inclusion_40_15::VERIFYINGKEY),
321        (_, 16) => Ok(&v2_non_inclusion_40_16::VERIFYINGKEY),
322        (_, 17) => Ok(&v2_non_inclusion_40_17::VERIFYINGKEY),
323        (_, 18) => Ok(&v2_non_inclusion_40_18::VERIFYINGKEY),
324        (_, 19) => Ok(&v2_non_inclusion_40_19::VERIFYINGKEY),
325        (_, 20) => Ok(&v2_non_inclusion_40_20::VERIFYINGKEY),
326        (_, 21) => Ok(&v2_non_inclusion_40_21::VERIFYINGKEY),
327        (_, 22) => Ok(&v2_non_inclusion_40_22::VERIFYINGKEY),
328        (_, 23) => Ok(&v2_non_inclusion_40_23::VERIFYINGKEY),
329        (_, 24) => Ok(&v2_non_inclusion_40_24::VERIFYINGKEY),
330        (_, 25) => Ok(&v2_non_inclusion_40_25::VERIFYINGKEY),
331        (_, 26) => Ok(&v2_non_inclusion_40_26::VERIFYINGKEY),
332        (_, 27) => Ok(&v2_non_inclusion_40_27::VERIFYINGKEY),
333        (_, 28) => Ok(&v2_non_inclusion_40_28::VERIFYINGKEY),
334        (_, 29) => Ok(&v2_non_inclusion_40_29::VERIFYINGKEY),
335        (_, 30) => Ok(&v2_non_inclusion_40_30::VERIFYINGKEY),
336        (_, 31) => Ok(&v2_non_inclusion_40_31::VERIFYINGKEY),
337        (_, 32) => Ok(&v2_non_inclusion_40_32::VERIFYINGKEY),
338
339        // Invalid configuration
340        _ => Err(InvalidPublicInputsLength),
341    }
342}
343
344#[inline(never)]
345pub fn verify<const N: usize>(
346    public_inputs: &[[u8; 32]; N],
347    proof: &CompressedProof,
348    vk: &Groth16Verifyingkey,
349) -> Result<(), VerifierError> {
350    let proof_a = decompress_g1(&proof.a).map_err(|_| DecompressG1Failed)?;
351    let proof_b = decompress_g2(&proof.b).map_err(|_| DecompressG2Failed)?;
352    let proof_c = decompress_g1(&proof.c).map_err(|_| DecompressG1Failed)?;
353    let mut verifier = Groth16Verifier::new(&proof_a, &proof_b, &proof_c, public_inputs, vk)
354        .map_err(|_| {
355            #[cfg(all(target_os = "solana", feature = "solana"))]
356            {
357                use solana_msg::msg;
358                msg!("Proof verification failed");
359                msg!("Public inputs: {:?}", public_inputs);
360                msg!("Proof A: {:?}", proof_a);
361                msg!("Proof B: {:?}", proof_b);
362                msg!("Proof C: {:?}", proof_c);
363            }
364            CreateGroth16VerifierFailed
365        })?;
366    verifier.verify().map_err(|_| {
367        #[cfg(all(target_os = "solana", feature = "solana"))]
368        {
369            use solana_msg::msg;
370            msg!("Proof verification failed");
371            msg!("Public inputs: {:?}", public_inputs);
372            msg!("Proof A: {:?}", proof_a);
373            msg!("Proof B: {:?}", proof_b);
374            msg!("Proof C: {:?}", proof_c);
375        }
376        ProofVerificationFailed
377    })?;
378    Ok(())
379}
380
381#[inline(never)]
382pub fn verify_batch_append_with_proofs(
383    batch_size: u64,
384    public_input_hash: [u8; 32],
385    compressed_proof: &CompressedProof,
386) -> Result<(), VerifierError> {
387    match batch_size {
388        10 => verify::<1>(
389            &[public_input_hash],
390            compressed_proof,
391            &batch_append_32_10::VERIFYINGKEY,
392        ),
393        500 => verify::<1>(
394            &[public_input_hash],
395            compressed_proof,
396            &batch_append_32_500::VERIFYINGKEY,
397        ),
398        _ => Err(InvalidPublicInputsLength),
399    }
400}
401
402#[inline(never)]
403pub fn verify_batch_update(
404    batch_size: u64,
405    public_input_hash: [u8; 32],
406    compressed_proof: &CompressedProof,
407) -> Result<(), VerifierError> {
408    match batch_size {
409        10 => verify::<1>(
410            &[public_input_hash],
411            compressed_proof,
412            &batch_update_32_10::VERIFYINGKEY,
413        ),
414        500 => verify::<1>(
415            &[public_input_hash],
416            compressed_proof,
417            &batch_update_32_500::VERIFYINGKEY,
418        ),
419        _ => Err(InvalidPublicInputsLength),
420    }
421}
422
423#[inline(never)]
424pub fn verify_batch_address_update(
425    batch_size: u64,
426    public_input_hash: [u8; 32],
427    compressed_proof: &CompressedProof,
428) -> Result<(), VerifierError> {
429    match batch_size {
430        10 => verify::<1>(
431            &[public_input_hash],
432            compressed_proof,
433            &batch_address_append_40_10::VERIFYINGKEY,
434        ),
435        250 => verify::<1>(
436            &[public_input_hash],
437            compressed_proof,
438            &batch_address_append_40_250::VERIFYINGKEY,
439        ),
440        _ => Err(InvalidPublicInputsLength),
441    }
442}