From 8375be357b118596470c6647265e211e5156f54d Mon Sep 17 00:00:00 2001 From: Yuwen Zhang Date: Wed, 16 Oct 2024 14:51:29 -0700 Subject: [PATCH 1/8] more readme fix --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 469cd31..27bf7b8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This crate verifies Groth16 proofs generated with SP1, leveraging Solana's BN254 ## Repository Overview -The `sp1-solana` library itself is in the [`verifier`](verifier) directory. `example/program` contains an example Solana program that uses this library to verify SP1 proofs, and `example/script` contains an example Solana script that invokes this program. +The `sp1-solana` library itself is in the [`verifier`](verifier) directory. [`example/program`](example/program) contains an example Solana program that uses this library to verify SP1 proofs, and [`example/script`](example/script) contains an example Solana script that invokes this program. ## Features @@ -21,7 +21,7 @@ The `sp1-solana` library itself is in the [`verifier`](verifier) directory. `exa - Rust - Edge Solana CLI - - Install with + - Install with the following command: ```shell sh -c "$(curl -sSfL https://release.anza.xyz/edge/install)" ``` @@ -36,13 +36,12 @@ from the pre-generated proof [`fibonacci_proof.bin`](../proofs/fibonacci_proof.b proves that the 20th fibonacci number is 6765. Optionally, this proof can be freshly generated from the [`sp1-program`](../sp1-program). -2. Extract the proof, public inputs, and program vkey hash from the `SP1ProofWithPublicValues`. +2. Extract the proof and public inputs from the `SP1ProofWithPublicValues`. * The `proof` is the Groth16 proof, serialized in [SP1's standard format](https://docs.rs/sp1-sdk/2.0.0/sp1_sdk/proof/struct.SP1ProofWithPublicValues.html#method.bytes) * The `sp1_public_inputs` are the public inputs to the underlying sp1 program. -* The `program_vkey_hash` is the hash of the underlying sp1 program's verification key. -Here is a snippet that demonstrates this extraction. +Here is a snippet from the [example script](./example/script/src/main.rs) that demonstrates this. ```rust @@ -68,9 +67,9 @@ run_verify_instruction(groth16_proof).await; ``` 3. Using the [`solana-program-test`](https://docs.rs/solana-program-test/latest/solana_program_test/) framework, send the `SP1Groth16Proof` to the -[`fibonacci-verifier-contract`](./program). This smart contract will verify the proof using the `sp1-solana` -crate against the fibonacci sp1 program vkey and print out the public inputs. Here is a snippet that demonstrates -how to do some common operations on the SP1 Groth16 proof. +[`fibonacci-verifier-contract`](./example/program). This smart contract will verify the proof using the `sp1-solana` +crate against the fibonacci SP1 program vkey and print out the public inputs. Here is a snippet that demonstrates +how to perform the verification and read the public inputs on chain. ```rust // Derived by running `vk.bytes32()` on the program's vkey. From 5f7ed8753cf6a2b7b30170ffa2f998f199c7e538 Mon Sep 17 00:00:00 2001 From: Yuwen Zhang Date: Wed, 16 Oct 2024 17:31:06 -0700 Subject: [PATCH 2/8] ratans doc fixes --- Cargo.lock | 3 + example/program/.DS_Store | Bin 0 -> 6148 bytes example/program/src/lib.rs | 13 +- example/script/Cargo.toml | 1 + example/script/src/main.rs | 6 +- verifier/Cargo.toml | 2 + verifier/src/lib.rs | 292 +++---------------------------------- verifier/src/test.rs | 18 ++- verifier/src/utils.rs | 273 ++++++++++++++++++++++++++++++++++ 9 files changed, 332 insertions(+), 276 deletions(-) create mode 100644 example/program/.DS_Store create mode 100644 verifier/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 25e7389..fc64272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2394,6 +2394,7 @@ version = "0.1.0" dependencies = [ "clap", "fibonacci-verifier-contract", + "hex", "solana-program-test", "solana-sdk", "sp1-build", @@ -8624,6 +8625,8 @@ dependencies = [ "ark-serialize 0.4.2", "borsh 1.5.1", "groth16-solana", + "hex", + "hex-literal 0.3.4", "num-bigint 0.4.6", "num-traits", "sha2 0.10.8", diff --git a/example/program/.DS_Store b/example/program/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ee771938787a1bb39cadddab0eb4f7463f2ead2b GIT binary patch literal 6148 zcmeHKI|>3Z5S>vG!N$@uSMUZw^aOhW1;s`bM6I{-TprCgpGH?ZZR8D1UNV`NkXP*N zh=|TFo0-T&L`HB!x!KS)+c)o6FCz+si0p zwti-h{ zq5pp-aYY5Fz+Wk#gGIBL<4IXtJCCzkTi`3W<=o+Bm^%f7mt&xpV=Sy3PdzE}ip{ZK V6Wc(iBkpt{e+En!8Ws4p0uNe26{i3I literal 0 HcmV?d00001 diff --git a/example/program/src/lib.rs b/example/program/src/lib.rs index e4af36c..f713e0e 100644 --- a/example/program/src/lib.rs +++ b/example/program/src/lib.rs @@ -11,9 +11,16 @@ use solana_program::entrypoint; #[cfg(not(feature = "no-entrypoint"))] entrypoint!(process_instruction); -// Derived by running `vk.bytes32()` on the program's vkey. -const FIBONACCI_VKEY_HASH: [u8; 32] = - hex_literal::hex!("0083e8e370d7f0d1c463337f76c9a60b62ad7cc54c89329107c92c1e62097872"); +#[cfg(not(doctest))] +/// Derived as follows: +/// +/// ``` +/// let client = sp1_sdk::ProverClient::new(); +/// let (pk, vk) = client.setup(YOUR_ELF_HERE); +/// let vkey_hash = vk.bytes32(); +/// ``` +const FIBONACCI_VKEY_HASH: &str = + "0x0083e8e370d7f0d1c463337f76c9a60b62ad7cc54c89329107c92c1e62097872"; /// The instruction data for the program. #[derive(BorshDeserialize, BorshSerialize)] diff --git a/example/script/Cargo.toml b/example/script/Cargo.toml index 3992a36..bf6b798 100644 --- a/example/script/Cargo.toml +++ b/example/script/Cargo.toml @@ -13,6 +13,7 @@ tokio.workspace = true clap = { version = "4.0", features = ["derive"] } sp1-sdk = "2.0.0" +hex = "0.4.3" [build-dependencies] sp1-build = "2.0.0" \ No newline at end of file diff --git a/example/script/src/main.rs b/example/script/src/main.rs index 956827a..ded3cba 100644 --- a/example/script/src/main.rs +++ b/example/script/src/main.rs @@ -21,8 +21,10 @@ struct Cli { prove: bool, } +/// The ELF binary of the SP1 program. const ELF: &[u8] = include_bytes!("../../sp1-program/elf/riscv32im-succinct-zkvm-elf"); +/// Invokes the solana program using Solana Program Test. async fn run_verify_instruction(groth16_proof: SP1Groth16Proof) { let program_id = Pubkey::new_unique(); @@ -62,7 +64,9 @@ async fn main() { if args.prove { // Initialize the prover client let client = ProverClient::new(); - let (pk, _vk) = client.setup(ELF); + let (pk, vk) = client.setup(ELF); + + println!("vkey bytes {:?}", sp1_sdk::HashableKey::bytes32(&vk)); // In our SP1 program, compute the 20th fibonacci number. let mut stdin = SP1Stdin::new(); diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 772d64f..25a09f3 100644 --- a/verifier/Cargo.toml +++ b/verifier/Cargo.toml @@ -20,7 +20,9 @@ ark-serialize = "0.4.2" ark-ff = "0.4.2" groth16-solana = { git = "https://github.com/sp1-patches/groth16-solana", branch = "patch-v0.0.3" } thiserror = "1.0.63" +hex = "0.4.3" [dev-dependencies] sp1-sdk = { version = "2.0.0", default-features = false} +hex-literal = "0.3.1" num-traits = { version = "0.2.19" } diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs index a415b26..7dd552e 100644 --- a/verifier/src/lib.rs +++ b/verifier/src/lib.rs @@ -1,255 +1,18 @@ -use ark_bn254::{Fq, G1Affine}; -use ark_ff::PrimeField; -use ark_serialize::CanonicalSerialize; -use borsh::BorshSerialize; use groth16_solana::groth16::Groth16Verifyingkey; use sha2::{Digest, Sha256}; -use thiserror::Error; - -/// Groth16 verification keys for different SP1 versions. -pub const GROTH16_VK_1_2_0_BYTES: &[u8] = include_bytes!("../vk/v1.2.0/groth16_vk.bin"); -pub const GROTH16_VK_2_0_0_BYTES: &[u8] = include_bytes!("../vk/v2.0.0/groth16_vk.bin"); #[cfg(test)] mod test; -#[derive(Error, Debug)] -pub enum Error { - #[error("G1 compression error")] - G1CompressionError, - #[error("G2 compression error")] - G2CompressionError, - #[error("Verification error")] - VerificationError, - #[error("Invalid public input")] - InvalidPublicInput, - #[error("Serialization error")] - SerializationError, - #[error("Deserialization error")] - DeserializationError, - #[error("Invalid instruction data")] - InvalidInstructionData, - #[error("Arithmetic error")] - ArithmeticError, - #[error("Pairing error")] - PairingError, - #[error("Invalid input")] - InvalidInput, - #[error("Borsh serialization error")] - BorshSerializeError, - #[error("Borsh deserialization error")] - BorshDeserializeError, - #[error("IO error")] - IoError, - #[error("Groth16 vkey hash mismatch")] - Groth16VkeyHashMismatch, -} - -const SCALAR_LEN: usize = 32; -const G1_LEN: usize = 64; -const G2_LEN: usize = 128; - -/// Everything needed to verify a Groth16 proof. -#[allow(dead_code)] -pub struct Verifier<'a, const N_PUBLIC: usize> { - /// The proof to verify. - proof: &'a Proof, - /// The public inputs to the proof. - public: &'a PublicInputs, - /// The verification key. - vk: &'a VerificationKey, -} - -/// A Groth16 proof. -/// -/// All Group elements are represented in uncompressed form. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Proof { - pub pi_a: [u8; 64], - pub pi_b: [u8; 128], - pub pi_c: [u8; 64], -} - -/// A generic Groth16 verification key over BN254. -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize)] -pub struct VerificationKey { - pub nr_pubinputs: u32, - pub vk_alpha_g1: [u8; G1_LEN], - pub vk_beta_g2: [u8; G2_LEN], - pub vk_gamma_g2: [u8; G2_LEN], - pub vk_delta_g2: [u8; G2_LEN], - pub vk_ic: Vec<[u8; G1_LEN]>, -} - -/// The public inputs for a Groth16 proof. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PublicInputs { - pub inputs: [[u8; SCALAR_LEN]; N], -} - -/// Convert the endianness of a byte array, chunk by chunk. -/// -/// Taken from https://github.com/anza-xyz/agave/blob/c54d840/curves/bn254/src/compression.rs#L176-L189 -fn convert_endianness( - bytes: &[u8; ARRAY_SIZE], -) -> [u8; ARRAY_SIZE] { - let reversed: [_; ARRAY_SIZE] = bytes - .chunks_exact(CHUNK_SIZE) - .flat_map(|chunk| chunk.iter().rev().copied()) - .enumerate() - .fold([0u8; ARRAY_SIZE], |mut acc, (i, v)| { - acc[i] = v; - acc - }); - reversed -} - -fn decompress_g1(g1_bytes: &[u8; 32]) -> Result<[u8; 64], Error> { - let g1_bytes = gnark_compressed_x_to_ark_compressed_x(g1_bytes)?; - let g1_bytes = convert_endianness::<32, 32>(&g1_bytes.as_slice().try_into().unwrap()); - groth16_solana::decompression::decompress_g1(&g1_bytes).map_err(|_| Error::G1CompressionError) -} - -fn decompress_g2(g2_bytes: &[u8; 64]) -> Result<[u8; 128], Error> { - let g2_bytes = gnark_compressed_x_to_ark_compressed_x(g2_bytes)?; - let g2_bytes = convert_endianness::<64, 64>(&g2_bytes.as_slice().try_into().unwrap()); - groth16_solana::decompression::decompress_g2(&g2_bytes).map_err(|_| Error::G2CompressionError) -} - -const GNARK_MASK: u8 = 0b11 << 6; -const GNARK_COMPRESSED_POSTIVE: u8 = 0b10 << 6; -const GNARK_COMPRESSED_NEGATIVE: u8 = 0b11 << 6; -const GNARK_COMPRESSED_INFINITY: u8 = 0b01 << 6; - -const ARK_MASK: u8 = 0b11 << 6; -const ARK_COMPRESSED_POSTIVE: u8 = 0b00 << 6; -const ARK_COMPRESSED_NEGATIVE: u8 = 0b10 << 6; -const ARK_COMPRESSED_INFINITY: u8 = 0b01 << 6; - -fn gnark_flag_to_ark_flag(msb: u8) -> Result { - let gnark_flag = msb & GNARK_MASK; - - let ark_flag = match gnark_flag { - GNARK_COMPRESSED_POSTIVE => ARK_COMPRESSED_POSTIVE, - GNARK_COMPRESSED_NEGATIVE => ARK_COMPRESSED_NEGATIVE, - GNARK_COMPRESSED_INFINITY => ARK_COMPRESSED_INFINITY, - _ => { - return Err(Error::InvalidInput); - } - }; - - Ok(msb & !ARK_MASK | ark_flag) -} - -fn gnark_compressed_x_to_ark_compressed_x(x: &[u8]) -> Result, Error> { - if x.len() != 32 && x.len() != 64 { - return Err(Error::InvalidInput); - } - let mut x_copy = x.to_owned(); - - let msb = gnark_flag_to_ark_flag(x_copy[0])?; - x_copy[0] = msb; +mod utils; +use utils::{ + decode_sp1_vkey_hash, groth16_public_values, load_groth16_verifying_key_from_bytes, + load_proof_from_bytes, load_public_inputs_from_bytes, Error, +}; - x_copy.reverse(); - Ok(x_copy) -} - -fn uncompressed_bytes_to_g1_point(buf: &[u8]) -> Result { - if buf.len() != 64 { - return Err(Error::InvalidInput); - }; - - let (x_bytes, y_bytes) = buf.split_at(32); - - let x = Fq::from_be_bytes_mod_order(x_bytes); - let y = Fq::from_be_bytes_mod_order(y_bytes); - - Ok(G1Affine::new_unchecked(x, y)) -} - -fn negate_g1(g1_bytes: &[u8; 64]) -> Result<[u8; 64], Error> { - let g1 = -uncompressed_bytes_to_g1_point(g1_bytes)?; - let mut g1_bytes = [0u8; 64]; - g1.serialize_uncompressed(&mut g1_bytes[..]) - .map_err(|_| Error::G1CompressionError)?; - Ok(convert_endianness::<32, 64>( - &g1_bytes.as_slice().try_into().unwrap(), - )) -} - -fn load_proof_from_bytes(buffer: &[u8]) -> Result { - Ok(Proof { - pi_a: negate_g1( - &buffer[..64] - .try_into() - .map_err(|_| Error::G1CompressionError)?, - )?, - pi_b: buffer[64..192] - .try_into() - .map_err(|_| Error::G2CompressionError)?, - pi_c: buffer[192..256] - .try_into() - .map_err(|_| Error::G1CompressionError)?, - }) -} - -fn load_groth16_verifying_key_from_bytes(buffer: &[u8]) -> Result { - // Note that g1_beta and g1_delta are not used in the verification process. - let g1_alpha = decompress_g1(buffer[..32].try_into().unwrap())?; - let g2_beta = decompress_g2(buffer[64..128].try_into().unwrap())?; - let g2_gamma = decompress_g2(buffer[128..192].try_into().unwrap())?; - let g2_delta = decompress_g2(buffer[224..288].try_into().unwrap())?; - - let num_k = u32::from_be_bytes([buffer[288], buffer[289], buffer[290], buffer[291]]); - let mut k = Vec::new(); - let mut offset = 292; - for _ in 0..num_k { - let point = decompress_g1(&buffer[offset..offset + 32].try_into().unwrap())?; - k.push(point); - offset += 32; - } - - let num_of_array_of_public_and_commitment_committed = u32::from_be_bytes([ - buffer[offset], - buffer[offset + 1], - buffer[offset + 2], - buffer[offset + 3], - ]); - offset += 4; - for _ in 0..num_of_array_of_public_and_commitment_committed { - let num = u32::from_be_bytes([ - buffer[offset], - buffer[offset + 1], - buffer[offset + 2], - buffer[offset + 3], - ]); - offset += 4; - for _ in 0..num { - offset += 4; - } - } - - Ok(VerificationKey { - vk_alpha_g1: g1_alpha, - vk_beta_g2: g2_beta, - vk_gamma_g2: g2_gamma, - vk_delta_g2: g2_delta, - vk_ic: k.clone(), - nr_pubinputs: num_of_array_of_public_and_commitment_committed, - }) -} - -fn load_public_inputs_from_bytes(buffer: &[u8]) -> Result, Error> { - let mut bytes = [0u8; 64]; - bytes[1..].copy_from_slice(buffer); // vkey_hash is 31 bytes - - Ok(PublicInputs::<2> { - inputs: [ - bytes[..32].try_into().map_err(|_| Error::InvalidInput)?, // vkey_hash - bytes[32..].try_into().map_err(|_| Error::InvalidInput)?, // committed_values_digest - ], - }) -} +/// Groth16 verification keys for different SP1 versions. +pub const GROTH16_VK_1_2_0_BYTES: &[u8] = include_bytes!("../vk/v1.2.0/groth16_vk.bin"); +pub const GROTH16_VK_2_0_0_BYTES: &[u8] = include_bytes!("../vk/v2.0.0/groth16_vk.bin"); /// Verifies a proof using raw bytes, without any checks. /// @@ -288,48 +51,37 @@ pub fn verify_proof_raw(proof: &[u8], public_inputs: &[u8], vk: &[u8]) -> Result } } -/// Hashes the public inputs in the same format as the Groth16 verifier. -pub fn hash_public_inputs(public_inputs: &[u8]) -> [u8; 32] { - let mut result = Sha256::digest(public_inputs); - - // Zero out the first 3 bits. - result[0] = result[0] & 0x1F; - - result.into() -} - -/// Formats the sp1 vkey hash and public inputs for use in the Groth16 verifier. -pub fn groth16_public_values(sp1_vkey_hash: &[u8; 32], sp1_public_inputs: &[u8]) -> Vec { - let committed_values_digest = hash_public_inputs(sp1_public_inputs); - [ - sp1_vkey_hash[1..].to_vec(), - committed_values_digest.to_vec(), - ] - .concat() -} - -/// Verifies a proof generated by [`SP1ProofWithPublicValues.bytes()`]. +/// Verifies a proof generated by [`SP1ProofWithPublicValues`]. /// -/// Checks the Groth16 vkey hash embedded in the `proof` against the provided groth16 vkey. +/// The proof is expected to be from this method on `SP1ProofWithPublicValues`: +/// https://docs.rs/sp1-sdk/latest/sp1_sdk/proof/struct.SP1ProofWithPublicValues.html#method.bytes +/// The public inputs are directly taken from the `SP1PublicValues`. +/// https://docs.rs/sp1-sdk/latest/sp1_sdk/struct.SP1PublicValues.html#method.as_slice +/// The vkey hash is derived from running `vk.bytes32()` on the program's vkey. +/// https://docs.rs/sp1-sdk/latest/sp1_sdk/trait.HashableKey.html#method.bytes32 #[inline] pub fn verify_proof( proof: &[u8], sp1_public_inputs: &[u8], - sp1_vkey_hash: &[u8; 32], + sp1_vkey_hash: &str, groth16_vk: &[u8], ) -> Result<(), Error> { // Hash the vk and get the first 4 bytes. let groth16_vk_hash: [u8; 4] = Sha256::digest(groth16_vk)[..4].try_into().unwrap(); - // Compare against the groth16_proof's groth16 vkey hash. + // Check to make sure that this proof was generated by the same groth16 vkey. + // SP1 prepends the raw Groth16 proof with the first 4 bytes of the groth16 vkey to + // faciliate this check. if groth16_vk_hash != proof[..4] { return Err(Error::Groth16VkeyHashMismatch); } + let sp1_vkey_hash = decode_sp1_vkey_hash(sp1_vkey_hash)?; + // Verify the proof. verify_proof_raw( &proof[4..], - &groth16_public_values(sp1_vkey_hash, sp1_public_inputs), + &groth16_public_values(&sp1_vkey_hash, sp1_public_inputs), groth16_vk, ) } diff --git a/verifier/src/test.rs b/verifier/src/test.rs index eab55c1..057ae51 100644 --- a/verifier/src/test.rs +++ b/verifier/src/test.rs @@ -29,10 +29,12 @@ fn test_verify_from_sp1() { padded_vkey_hash.extend_from_slice(&vkey_hash); let vkey_hash = padded_vkey_hash; + let sp1_vkey_hash = format!("0x{}", hex::encode(vkey_hash)); + assert!(verify_proof( &proof_bytes, &sp1_public_inputs, - &vkey_hash[..32].try_into().unwrap(), + &sp1_vkey_hash, &GROTH16_VK_2_0_0_BYTES ) .is_ok()); @@ -40,7 +42,7 @@ fn test_verify_from_sp1() { #[test] fn test_hash_public_inputs_() { - use crate::hash_public_inputs; + use crate::utils::hash_public_inputs; // Read the serialized SP1ProofWithPublicValues from the file. let sp1_proof_with_public_values_file = "../proofs/fibonacci_proof.bin"; @@ -61,3 +63,15 @@ fn test_hash_public_inputs_() { hash_public_inputs(&sp1_proof_with_public_values.public_values.to_vec()) ); } + +#[test] +fn test_decode_sp1_vkey_hash() { + use crate::utils::decode_sp1_vkey_hash; + + let sp1_vkey_hash = "0x0054c0e58911dd8b993c6d8f249aa50a2e523114ec4b7ef9dd355c5f6bfbf3ce"; + let decoded_sp1_vkey_hash = decode_sp1_vkey_hash(sp1_vkey_hash).unwrap(); + assert_eq!( + decoded_sp1_vkey_hash, + hex_literal::hex!("0054c0e58911dd8b993c6d8f249aa50a2e523114ec4b7ef9dd355c5f6bfbf3ce") + ); +} diff --git a/verifier/src/utils.rs b/verifier/src/utils.rs new file mode 100644 index 0000000..71055b6 --- /dev/null +++ b/verifier/src/utils.rs @@ -0,0 +1,273 @@ +use ark_bn254::{Fq, G1Affine}; +use ark_ff::PrimeField; +use ark_serialize::CanonicalSerialize; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("G1 compression error")] + G1CompressionError, + #[error("G2 compression error")] + G2CompressionError, + #[error("Verification error")] + VerificationError, + #[error("Invalid public input")] + InvalidPublicInput, + #[error("Serialization error")] + SerializationError, + #[error("Deserialization error")] + DeserializationError, + #[error("Invalid instruction data")] + InvalidInstructionData, + #[error("Arithmetic error")] + ArithmeticError, + #[error("Pairing error")] + PairingError, + #[error("Invalid input")] + InvalidInput, + #[error("Borsh serialization error")] + BorshSerializeError, + #[error("Borsh deserialization error")] + BorshDeserializeError, + #[error("IO error")] + IoError, + #[error("Groth16 vkey hash mismatch")] + Groth16VkeyHashMismatch, + #[error("Invalid program vkey hash")] + InvalidProgramVkeyHash, +} + +const SCALAR_LEN: usize = 32; +const G1_LEN: usize = 64; +const G2_LEN: usize = 128; + +/// Everything needed to verify a Groth16 proof. +#[allow(dead_code)] +pub struct Verifier<'a, const N_PUBLIC: usize> { + /// The proof to verify. + proof: &'a Proof, + /// The public inputs to the proof. + public: &'a PublicInputs, + /// The verification key. + vk: &'a VerificationKey, +} + +/// A Groth16 proof. +/// +/// All Group elements are represented in uncompressed form. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Proof { + pub pi_a: [u8; 64], + pub pi_b: [u8; 128], + pub pi_c: [u8; 64], +} + +/// A generic Groth16 verification key over BN254. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerificationKey { + pub nr_pubinputs: u32, + pub vk_alpha_g1: [u8; G1_LEN], + pub vk_beta_g2: [u8; G2_LEN], + pub vk_gamma_g2: [u8; G2_LEN], + pub vk_delta_g2: [u8; G2_LEN], + pub vk_ic: Vec<[u8; G1_LEN]>, +} + +/// The public inputs for a Groth16 proof. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicInputs { + pub inputs: [[u8; SCALAR_LEN]; N], +} + +/// Convert the endianness of a byte array, chunk by chunk. +/// +/// Taken from https://github.com/anza-xyz/agave/blob/c54d840/curves/bn254/src/compression.rs#L176-L189 +fn convert_endianness( + bytes: &[u8; ARRAY_SIZE], +) -> [u8; ARRAY_SIZE] { + let reversed: [_; ARRAY_SIZE] = bytes + .chunks_exact(CHUNK_SIZE) + .flat_map(|chunk| chunk.iter().rev().copied()) + .enumerate() + .fold([0u8; ARRAY_SIZE], |mut acc, (i, v)| { + acc[i] = v; + acc + }); + reversed +} + +fn decompress_g1(g1_bytes: &[u8; 32]) -> Result<[u8; 64], Error> { + let g1_bytes = gnark_compressed_x_to_ark_compressed_x(g1_bytes)?; + let g1_bytes = convert_endianness::<32, 32>(&g1_bytes.as_slice().try_into().unwrap()); + groth16_solana::decompression::decompress_g1(&g1_bytes).map_err(|_| Error::G1CompressionError) +} + +fn decompress_g2(g2_bytes: &[u8; 64]) -> Result<[u8; 128], Error> { + let g2_bytes = gnark_compressed_x_to_ark_compressed_x(g2_bytes)?; + let g2_bytes = convert_endianness::<64, 64>(&g2_bytes.as_slice().try_into().unwrap()); + groth16_solana::decompression::decompress_g2(&g2_bytes).map_err(|_| Error::G2CompressionError) +} + +const GNARK_MASK: u8 = 0b11 << 6; +const GNARK_COMPRESSED_POSTIVE: u8 = 0b10 << 6; +const GNARK_COMPRESSED_NEGATIVE: u8 = 0b11 << 6; +const GNARK_COMPRESSED_INFINITY: u8 = 0b01 << 6; + +const ARK_MASK: u8 = 0b11 << 6; +const ARK_COMPRESSED_POSTIVE: u8 = 0b00 << 6; +const ARK_COMPRESSED_NEGATIVE: u8 = 0b10 << 6; +const ARK_COMPRESSED_INFINITY: u8 = 0b01 << 6; + +fn gnark_flag_to_ark_flag(msb: u8) -> Result { + let gnark_flag = msb & GNARK_MASK; + + let ark_flag = match gnark_flag { + GNARK_COMPRESSED_POSTIVE => ARK_COMPRESSED_POSTIVE, + GNARK_COMPRESSED_NEGATIVE => ARK_COMPRESSED_NEGATIVE, + GNARK_COMPRESSED_INFINITY => ARK_COMPRESSED_INFINITY, + _ => { + return Err(Error::InvalidInput); + } + }; + + Ok(msb & !ARK_MASK | ark_flag) +} + +fn gnark_compressed_x_to_ark_compressed_x(x: &[u8]) -> Result, Error> { + if x.len() != 32 && x.len() != 64 { + return Err(Error::InvalidInput); + } + let mut x_copy = x.to_owned(); + + let msb = gnark_flag_to_ark_flag(x_copy[0])?; + x_copy[0] = msb; + + x_copy.reverse(); + Ok(x_copy) +} + +fn uncompressed_bytes_to_g1_point(buf: &[u8]) -> Result { + if buf.len() != 64 { + return Err(Error::InvalidInput); + }; + + let (x_bytes, y_bytes) = buf.split_at(32); + + let x = Fq::from_be_bytes_mod_order(x_bytes); + let y = Fq::from_be_bytes_mod_order(y_bytes); + + Ok(G1Affine::new_unchecked(x, y)) +} + +fn negate_g1(g1_bytes: &[u8; 64]) -> Result<[u8; 64], Error> { + let g1 = -uncompressed_bytes_to_g1_point(g1_bytes)?; + let mut g1_bytes = [0u8; 64]; + g1.serialize_uncompressed(&mut g1_bytes[..]) + .map_err(|_| Error::G1CompressionError)?; + Ok(convert_endianness::<32, 64>( + &g1_bytes.as_slice().try_into().unwrap(), + )) +} + +pub(crate) fn load_proof_from_bytes(buffer: &[u8]) -> Result { + Ok(Proof { + pi_a: negate_g1( + &buffer[..64] + .try_into() + .map_err(|_| Error::G1CompressionError)?, + )?, + pi_b: buffer[64..192] + .try_into() + .map_err(|_| Error::G2CompressionError)?, + pi_c: buffer[192..256] + .try_into() + .map_err(|_| Error::G1CompressionError)?, + }) +} +pub(crate) fn load_groth16_verifying_key_from_bytes( + buffer: &[u8], +) -> Result { + // Note that g1_beta and g1_delta are not used in the verification process. + let g1_alpha = decompress_g1(buffer[..32].try_into().unwrap())?; + let g2_beta = decompress_g2(buffer[64..128].try_into().unwrap())?; + let g2_gamma = decompress_g2(buffer[128..192].try_into().unwrap())?; + let g2_delta = decompress_g2(buffer[224..288].try_into().unwrap())?; + + let num_k = u32::from_be_bytes([buffer[288], buffer[289], buffer[290], buffer[291]]); + let mut k = Vec::new(); + let mut offset = 292; + for _ in 0..num_k { + let point = decompress_g1(&buffer[offset..offset + 32].try_into().unwrap())?; + k.push(point); + offset += 32; + } + + let num_of_array_of_public_and_commitment_committed = u32::from_be_bytes([ + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + ]); + offset += 4; + for _ in 0..num_of_array_of_public_and_commitment_committed { + let num = u32::from_be_bytes([ + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + ]); + offset += 4; + for _ in 0..num { + offset += 4; + } + } + + Ok(VerificationKey { + vk_alpha_g1: g1_alpha, + vk_beta_g2: g2_beta, + vk_gamma_g2: g2_gamma, + vk_delta_g2: g2_delta, + vk_ic: k.clone(), + nr_pubinputs: num_of_array_of_public_and_commitment_committed, + }) +} + +pub(crate) fn load_public_inputs_from_bytes(buffer: &[u8]) -> Result, Error> { + let mut bytes = [0u8; 64]; + bytes[1..].copy_from_slice(buffer); // vkey_hash is 31 bytes + + Ok(PublicInputs::<2> { + inputs: [ + bytes[..32].try_into().map_err(|_| Error::InvalidInput)?, // vkey_hash + bytes[32..].try_into().map_err(|_| Error::InvalidInput)?, // committed_values_digest + ], + }) +} + +/// Hashes the public inputs in the same format as the Groth16 verifier. +pub fn hash_public_inputs(public_inputs: &[u8]) -> [u8; 32] { + let mut result = Sha256::digest(public_inputs); + + // The Groth16 verifier operates over a 254 bit field (BN254), so we need to zero + // out the first 3 bits. The same logic happens in the SP1 Ethereum verifier contract. + result[0] = result[0] & 0x1F; + + result.into() +} + +/// Formats the sp1 vkey hash and public inputs for use in the Groth16 verifier. +pub fn groth16_public_values(sp1_vkey_hash: &[u8; 32], sp1_public_inputs: &[u8]) -> Vec { + let committed_values_digest = hash_public_inputs(sp1_public_inputs); + [ + sp1_vkey_hash[1..].to_vec(), + committed_values_digest.to_vec(), + ] + .concat() +} + +/// Decodes the sp1 vkey hash from the string from bytes32. +pub fn decode_sp1_vkey_hash(sp1_vkey_hash: &str) -> Result<[u8; 32], Error> { + let bytes = hex::decode(&sp1_vkey_hash[2..]).map_err(|_| Error::InvalidProgramVkeyHash)?; + bytes.try_into().map_err(|_| Error::InvalidProgramVkeyHash) +} From a20ce86689521665db45421cceb8aef8c13a51bb Mon Sep 17 00:00:00 2001 From: Yuwen Zhang Date: Wed, 16 Oct 2024 17:49:53 -0700 Subject: [PATCH 3/8] remove dsstore --- .gitignore | 1 + example/program/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 example/program/.DS_Store diff --git a/.gitignore b/.gitignore index 3994d02..77d5979 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ target/ test_serialized_fixture.bin +.DS_Store diff --git a/example/program/.DS_Store b/example/program/.DS_Store deleted file mode 100644 index ee771938787a1bb39cadddab0eb4f7463f2ead2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKI|>3Z5S>vG!N$@uSMUZw^aOhW1;s`bM6I{-TprCgpGH?ZZR8D1UNV`NkXP*N zh=|TFo0-T&L`HB!x!KS)+c)o6FCz+si0p zwti-h{ zq5pp-aYY5Fz+Wk#gGIBL<4IXtJCCzkTi`3W<=o+Bm^%f7mt&xpV=Sy3PdzE}ip{ZK V6Wc(iBkpt{e+En!8Ws4p0uNe26{i3I From 440ef90c068f0f7cf409a38ba87694eaa5b1a7e7 Mon Sep 17 00:00:00 2001 From: Yuwen Zhang Date: Wed, 16 Oct 2024 17:56:55 -0700 Subject: [PATCH 4/8] nits --- Cargo.lock | 1 - example/script/Cargo.toml | 1 - verifier/src/lib.rs | 7 ++----- verifier/vk/v1.2.0/groth16_vk.bin | Bin 520 -> 0 bytes verifier/vk/v3.0.0rc4/groth16_vk.bin | Bin 0 -> 396 bytes 5 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 verifier/vk/v1.2.0/groth16_vk.bin create mode 100644 verifier/vk/v3.0.0rc4/groth16_vk.bin diff --git a/Cargo.lock b/Cargo.lock index fc64272..8469907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2394,7 +2394,6 @@ version = "0.1.0" dependencies = [ "clap", "fibonacci-verifier-contract", - "hex", "solana-program-test", "solana-sdk", "sp1-build", diff --git a/example/script/Cargo.toml b/example/script/Cargo.toml index bf6b798..3992a36 100644 --- a/example/script/Cargo.toml +++ b/example/script/Cargo.toml @@ -13,7 +13,6 @@ tokio.workspace = true clap = { version = "4.0", features = ["derive"] } sp1-sdk = "2.0.0" -hex = "0.4.3" [build-dependencies] sp1-build = "2.0.0" \ No newline at end of file diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs index 7dd552e..a14f937 100644 --- a/verifier/src/lib.rs +++ b/verifier/src/lib.rs @@ -5,13 +5,10 @@ use sha2::{Digest, Sha256}; mod test; mod utils; -use utils::{ - decode_sp1_vkey_hash, groth16_public_values, load_groth16_verifying_key_from_bytes, - load_proof_from_bytes, load_public_inputs_from_bytes, Error, -}; +use utils::*; /// Groth16 verification keys for different SP1 versions. -pub const GROTH16_VK_1_2_0_BYTES: &[u8] = include_bytes!("../vk/v1.2.0/groth16_vk.bin"); +pub const GROTH16_VK_3_0_0_RC4_BYTES: &[u8] = include_bytes!("../vk/v3.0.0rc4/groth16_vk.bin"); pub const GROTH16_VK_2_0_0_BYTES: &[u8] = include_bytes!("../vk/v2.0.0/groth16_vk.bin"); /// Verifies a proof using raw bytes, without any checks. diff --git a/verifier/vk/v1.2.0/groth16_vk.bin b/verifier/vk/v1.2.0/groth16_vk.bin deleted file mode 100644 index fc0f1a1d577cfaff82330cd657c4b26c715e05e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 zcmV+j0{8vpGI&n8Ga8F}&gc$alph5S{k{52C}({Et(f!d>(M@z?kmEr5V&$BxN#I* zsul429EBCC&oi%<{1fEy4ayiM_ie8zV7*@zn1yl-#X=+`I zJ!qF-uZs6VC~NWwD8mMT1WPXHpfP%so*bmnaAHtPFsy~@%QL2T?H!Qqj|j?n0VX)g zYT62*vC-Q(M|EFL@=hH_+K*;AzfySZV12$An`7|~$+-To7KRY64n%<`IA?sCkl5`H zX$F-88ATIv5b+n3bJUJ_`%7*_U{fs`onzK9x^xNV(AV4m000BhUxv8wP;v+;GKP+y zG)~-ula^1jcPgugw3(q-YG|R9Qx{#a3UOQOB$GdOEju~q%e;tDqVSpF>|xqYxYk7 diff --git a/verifier/vk/v3.0.0rc4/groth16_vk.bin b/verifier/vk/v3.0.0rc4/groth16_vk.bin new file mode 100644 index 0000000000000000000000000000000000000000..f2634ecebd9e3b6a44a4afe72ce49602c2f7875f GIT binary patch literal 396 zcmV;70dxMFh69@$94^+d2h_o-EQ>}#zVk-mywDv^@=;%ts1`V$V=vP?CDkZCw+02$ zk~WecHhxXw zcj2gQ6A0!20KbkX3BTVW#@#5=8o^3vP=vTwZA9eVr?oR2m{U|jd@6q3%7d#4$&Eu5 z#WejDo^L1NOAXLljYMwI?XV+9vPI6ue#8qR0KAaZV(}4XwF~bo1?35biZIE3H Date: Wed, 16 Oct 2024 18:12:03 -0700 Subject: [PATCH 5/8] more docs --- verifier/src/lib.rs | 19 +++++++++++++++++++ verifier/src/utils.rs | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs index a14f937..a54dfb8 100644 --- a/verifier/src/lib.rs +++ b/verifier/src/lib.rs @@ -1,3 +1,22 @@ +//! # Verifier +//! +//! This crate contains utilities for verifying SP1 Groth16 proofs on Solana. +//! +//! # Example +//! ``` +//! use sp1_sdk::proof::SP1ProofWithPublicValues; +//! use sp1_solana::{verify_proof, GROTH16_VK_2_0_0_BYTES}; +//! +//! let sp1_proof_with_public_values_file = "../proofs/fibonacci_proof.bin"; +//! let sp1_proof_with_public_values = +//! SP1ProofWithPublicValues::load(&sp1_proof_with_public_values_file).unwrap(); +//! let proof_bytes = sp1_proof_with_public_values.bytes(); +//! let sp1_public_inputs = sp1_proof_with_public_values.public_values.to_vec(); +//! let vkey_hash = "0x0083e8e370d7f0d1c463337f76c9a60b62ad7cc54c89329107c92c1e62097872"; +//! +//! verify_proof(&proof_bytes, &sp1_public_inputs, &vkey_hash, &GROTH16_VK_2_0_0_BYTES).unwrap(); +//! ``` + use groth16_solana::groth16::Groth16Verifyingkey; use sha2::{Digest, Sha256}; diff --git a/verifier/src/utils.rs b/verifier/src/utils.rs index 71055b6..80a707c 100644 --- a/verifier/src/utils.rs +++ b/verifier/src/utils.rs @@ -1,3 +1,9 @@ +//! Utility functions for the SP1 Groth16 Solana verifier. +//! +//! This module contains functions for decompressing G1 and G2 points, as well as +//! for loading proofs into a form appropriate for verification. This is necessary to coerce +//! SP1 Groth16 proofs into the form expected by the `groth16_solana` crate. + use ark_bn254::{Fq, G1Affine}; use ark_ff::PrimeField; use ark_serialize::CanonicalSerialize; From f397fa71a25cd555673effe26743e284d5eee84a Mon Sep 17 00:00:00 2001 From: Yuwen Zhang Date: Wed, 16 Oct 2024 18:34:37 -0700 Subject: [PATCH 6/8] more docs again --- verifier/src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs index a54dfb8..6c29142 100644 --- a/verifier/src/lib.rs +++ b/verifier/src/lib.rs @@ -7,11 +7,16 @@ //! use sp1_sdk::proof::SP1ProofWithPublicValues; //! use sp1_solana::{verify_proof, GROTH16_VK_2_0_0_BYTES}; //! +//! // Load the sp1_proof_with_public_values from a file. //! let sp1_proof_with_public_values_file = "../proofs/fibonacci_proof.bin"; //! let sp1_proof_with_public_values = //! SP1ProofWithPublicValues::load(&sp1_proof_with_public_values_file).unwrap(); +//! +//! // Fetch the proof and public inputs from the SP1ProofWithPublicValues. //! let proof_bytes = sp1_proof_with_public_values.bytes(); //! let sp1_public_inputs = sp1_proof_with_public_values.public_values.to_vec(); +//! +//! // Typically, the vkey hash is computed from `vk.bytes32()` on the SP1 program's vkey. //! let vkey_hash = "0x0083e8e370d7f0d1c463337f76c9a60b62ad7cc54c89329107c92c1e62097872"; //! //! verify_proof(&proof_bytes, &sp1_public_inputs, &vkey_hash, &GROTH16_VK_2_0_0_BYTES).unwrap(); @@ -85,7 +90,9 @@ pub fn verify_proof( // Hash the vk and get the first 4 bytes. let groth16_vk_hash: [u8; 4] = Sha256::digest(groth16_vk)[..4].try_into().unwrap(); - // Check to make sure that this proof was generated by the same groth16 vkey. + // Check to make sure that this proof was generated by the groth16 proving key corresponding to + // the given groth16_vk. + // // SP1 prepends the raw Groth16 proof with the first 4 bytes of the groth16 vkey to // faciliate this check. if groth16_vk_hash != proof[..4] { From 5820520f7c0ab50b316e9c88557cf91e7144708e Mon Sep 17 00:00:00 2001 From: Yuwen Zhang Date: Wed, 16 Oct 2024 19:19:05 -0700 Subject: [PATCH 7/8] nit --- example/script/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/example/script/src/main.rs b/example/script/src/main.rs index ded3cba..2882b65 100644 --- a/example/script/src/main.rs +++ b/example/script/src/main.rs @@ -66,7 +66,10 @@ async fn main() { let client = ProverClient::new(); let (pk, vk) = client.setup(ELF); - println!("vkey bytes {:?}", sp1_sdk::HashableKey::bytes32(&vk)); + println!( + "Program Verification Key Bytes {:?}", + sp1_sdk::HashableKey::bytes32(&vk) + ); // In our SP1 program, compute the 20th fibonacci number. let mut stdin = SP1Stdin::new(); From 0a58b6572c4cfff021d9490aac43abc094958335 Mon Sep 17 00:00:00 2001 From: Yuwen Zhang Date: Fri, 18 Oct 2024 14:05:22 -0700 Subject: [PATCH 8/8] fix vkey hash constant --- README.md | 12 ++++++++++-- example/program/src/lib.rs | 2 +- example/script/src/main.rs | 2 ++ proofs/fibonacci_proof.bin | Bin 1450 -> 1450 bytes 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 27bf7b8..c7d0543 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,16 @@ run_verify_instruction(groth16_proof).await; 3. Using the [`solana-program-test`](https://docs.rs/solana-program-test/latest/solana_program_test/) framework, send the `SP1Groth16Proof` to the [`fibonacci-verifier-contract`](./example/program). This smart contract will verify the proof using the `sp1-solana` -crate against the fibonacci SP1 program vkey and print out the public inputs. Here is a snippet that demonstrates -how to perform the verification and read the public inputs on chain. +crate against the fibonacci SP1 program vkey and print out the public inputs. + +> [!NOTE] +> In this example, a Groth16 proof and public values are directly passed into the contract as transaction data. +> In real use cases, this may not be reasonable, since the upper limit for transaction data is 1232 bytes. +> Groth16 proofs themselves are already 260 bytes, and public inputs can potentially be very large. +> See [this article](https://solana.com/developers/courses/program-optimization/lookup-tables) for a discussion +> on how to handle this. + +Here is a snippet that demonstrates how to perform the verification and read the public inputs on chain. ```rust // Derived by running `vk.bytes32()` on the program's vkey. diff --git a/example/program/src/lib.rs b/example/program/src/lib.rs index f713e0e..24b8b7d 100644 --- a/example/program/src/lib.rs +++ b/example/program/src/lib.rs @@ -20,7 +20,7 @@ entrypoint!(process_instruction); /// let vkey_hash = vk.bytes32(); /// ``` const FIBONACCI_VKEY_HASH: &str = - "0x0083e8e370d7f0d1c463337f76c9a60b62ad7cc54c89329107c92c1e62097872"; + "0x0054c0e58911dd8b993c6d8f249aa50a2e523114ec4b7ef9dd355c5f6bfbf3ce"; /// The instruction data for the program. #[derive(BorshDeserialize, BorshSerialize)] diff --git a/example/script/src/main.rs b/example/script/src/main.rs index 2882b65..3eecf88 100644 --- a/example/script/src/main.rs +++ b/example/script/src/main.rs @@ -93,6 +93,8 @@ async fn main() { sp1_public_inputs: sp1_proof_with_public_values.public_values.to_vec(), }; + println!("len of proof {:?}", groth16_proof.proof.len()); + // Send the proof to the contract, and verify it on `solana-program-test`. run_verify_instruction(groth16_proof).await; } diff --git a/proofs/fibonacci_proof.bin b/proofs/fibonacci_proof.bin index 97644452c13a19a325144b80ac39d640d533abca..385d535d7d9eea10f8bc0135c85ab58f14f9b436 100644 GIT binary patch literal 1450 zcmeHHJ&u?)5d9!g8Yn}PJ_SuQ|37Dls3Sz>8QTRCDIz6J3fi0{+A>D~X@x|EI6{s< zkC$wbBT&SWHL^YPX5O2TA6(b<{Or4hk}f9h02C2~n2rgiERaYbgaODFA*q>Wk(PP< z@*I>*L4;I^9DewOc;UH<^n-@vtQg6h;K1lek|3Ev4D12HiJ_)KL2(i)pu2%MDdMRG z==%B-Dq?%H4X0QHxN3{;>N$6?qppMDR#UTR=tiF`O2=y$PRmMMtUX!FcQ21Irg|%b zLYLthjw4SI*m4wrHMfo+@gBAkG!+3_s`qL#$Xrruph?c`kg+X%gKw+i|3W71aTs`x zqW086E3duD%;ua_C#!4sEaM6&mbu(=OKGgNT}}}~6;(sFcMZ%Qo3{WHA6;T=7#+T` z&Con;jIoBo;IL`9q2o?N=pA#eQsC2pHGQ4D?Uc!bWmv~LGK?5>&|^BU9^6MmVW{10 zh2;bx1hg5F4ny$aNdpzC4m%TSx|5{hPOPxloV}V$j>F=uV0ij^8nO@HLp-HhPBTel zKyrWt%%da^->XJr*sx+gL9!-C1$@=;GM>j8j8@GS_`IEl*?$;mj z?cc9HzP){#uiyXk`^|@UU)_I(5AW^cd;7Wh_OrKNo?O>o-(1(D`@1*v44%Qi*xYKl literal 1450 zcmeHHz0OcK481LgrHpi8XJA3`fBX!nGeVu5#2zY@0jXUWy7DZDkw+*JLh4lY5qJc4 zjQ9o~p~IIb@=f9#A0J!y&UIZ+dwm^9G?ZyzFF-Z$sXQ1N$b^(Y6$lX!hr5}mAXe=<(!G;2W z00XZdzo8;XIx{q_v0{_DdKDc3n#oW#c^5TFokxgbggO~Zm7%JxaiG@RM0(cYN}JJf z2JbBsNE~U?8Y^|}(4bgKc51n5>FhL7E303YW#n z3}YA#KvG3K0bc1s(a_PE^S*DT8!YC8b1Ncxu=|REOIF z-^)+0K0NY2-h6)l_TB3rZhnS$Z|s8``@OmU-QDjGuj}8RuIt{-*`N4iJQ-gA?2~F9