diff --git a/.circleci/config.yml b/.circleci/config.yml index ab6d4724e8..e518cea714 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1285,6 +1285,7 @@ workflows: or pipeline.git.branch == "canary" or pipeline.git.branch == "testnet" or pipeline.git.branch == "mainnet" + or pipeline.git.branch == "profile_verifier_multirecord" jobs: - check-unused-dependencies # This can be cleaned up before releases - check-cargo-semver-checks # This can be cleaned up before releases diff --git a/.gitignore b/.gitignore index 9e5be3c39d..fb68a7a443 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ **storage_db **/*.prover.* **/*.verifier.* +**/varuna_verifier_artifacts/ *.usrs +flamegraph.svg !**/powers-of-beta-15.usrs* !**/shifted-powers-of-beta-15.usrs* !**/powers-of-beta-16.usrs* diff --git a/Cargo.lock b/Cargo.lock index bcbbbc1f03..09942dd5f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3459,6 +3459,7 @@ dependencies = [ "expect-test", "hex", "k256", + "rayon", "serde", "serde_json", "smallvec", diff --git a/Cargo.toml b/Cargo.toml index ab9a01d257..843e436346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,6 +171,7 @@ dev-print = [ "snarkvm-utilities/dev-print", "snarkvm-algorithms/dev-print", "snarkvm-circuit/dev-print", + "snarkvm-console/dev-print", "snarkvm-curves/dev-print", "snarkvm-fields/dev-print", "snarkvm-parameters/dev-print", diff --git a/algorithms/Cargo.toml b/algorithms/Cargo.toml index c75f8a6c3d..0b871c7916 100644 --- a/algorithms/Cargo.toml +++ b/algorithms/Cargo.toml @@ -45,6 +45,12 @@ path = "benches/snark/varuna.rs" harness = false required-features = [ "test" ] +[[bench]] +name = "varuna_verifier" +path = "benches/snark/varuna_verifier.rs" +harness = false +required-features = [ "test" ] + [dependencies.snarkvm-curves] workspace = true diff --git a/algorithms/benches/snark/varuna.rs b/algorithms/benches/snark/varuna.rs index 8ccba91ff7..3bd6475cbe 100644 --- a/algorithms/benches/snark/varuna.rs +++ b/algorithms/benches/snark/varuna.rs @@ -229,6 +229,86 @@ fn snark_batch_verify(c: &mut Criterion) { }); } +fn snark_batch_verify_scaling(c: &mut Criterion) { + let rng = &mut TestRng::default(); + + let batch_specs = vec![ + // [many x (batch_size, num_constraints, num_variables, num_public_inputs)]) + vec![(1, 10_000, 5_000, 16)], + vec![(1, 50_000, 25_000, 64)], + vec![(30, 50_000, 25_000, 64)], + vec![(30, 50_000, 25_000, 64), (1, 5_000_000, 5_000_000, 1024)], + ]; + + let max_vars = *batch_specs + .iter() + .map(|batches| batches.iter().map(|(_, _, num_variables, _)| num_variables).max().unwrap()) + .max() + .unwrap(); + let max_constraints = *batch_specs + .iter() + .map(|batches| batches.iter().map(|(_, num_constraints, _, _)| num_constraints).max().unwrap()) + .max() + .unwrap(); + let max_density = 2 * max_constraints; + + let max_degree = AHPForR1CS::::max_degree(max_constraints, max_vars, max_density).unwrap(); + let universal_srs = VarunaInst::universal_setup(max_degree).unwrap(); + let universal_prover = &universal_srs.to_universal_prover().unwrap(); + let universal_verifier = &universal_srs.to_universal_verifier().unwrap(); + let fs_parameters = FS::sample_parameters(); + + let varuna_version = VarunaVersion::V3; + + for batch_spec in batch_specs { + let batch_str = batch_spec + .iter() + .map(|(batch_size, num_constraints, _, num_public_inputs)| { + format!("({batch_size} x [{num_public_inputs}, {num_constraints}])") + }) + .collect::>() + .join(" + "); + + let circuits_and_inputs = batch_spec + .iter() + .map(|&batch| { + let (batch_size, num_constraints, num_variables, num_public_inputs) = batch; + let (circuit, public_inputs) = + TestCircuit::gen_rand(num_public_inputs, num_constraints, num_variables, rng); + ( + VarunaInst::circuit_setup(&universal_srs, &circuit).unwrap(), + vec![circuit; batch_size], + vec![public_inputs; batch_size], + ) + }) + .collect::>(); + + let pks_to_circuits = circuits_and_inputs + .iter() + .map(|((pk, _), circuits, _)| (pk, circuits.as_slice())) + .collect::>(); + let vks_to_inputs = + circuits_and_inputs.iter().map(|((_, vk), _, inputs)| (vk, inputs.as_slice())).collect::>(); + + let proof = + VarunaInst::prove_batch(universal_prover, &fs_parameters, varuna_version, &pks_to_circuits, rng).unwrap(); + + c.bench_function(&format!("snark_batch_verify_scaling/{batch_str}"), |b| { + b.iter(|| { + let verification = VarunaInst::verify_batch( + universal_verifier, + &fs_parameters, + varuna_version, + &vks_to_inputs, + &proof, + ) + .unwrap(); + assert!(verification); + }); + }); + } +} + fn snark_vk_serialize(c: &mut Criterion) { use snarkvm_utilities::serialize::Compress; let mut group = c.benchmark_group("snark_vk_serialize"); @@ -344,7 +424,7 @@ fn snark_certificate_verify(c: &mut Criterion) { criterion_group! { name = varuna_snark; config = Criterion::default().measurement_time(Duration::from_secs(10)); - targets = snark_universal_setup, snark_circuit_setup, snark_prove, snark_verify, snark_batch_prove, snark_batch_verify, snark_vk_serialize, snark_vk_deserialize, snark_certificate_prove, snark_certificate_verify, + targets = snark_universal_setup, snark_circuit_setup, snark_prove, snark_verify, snark_batch_prove, snark_batch_verify, snark_batch_verify_scaling, snark_vk_serialize, snark_vk_deserialize, snark_certificate_prove, snark_certificate_verify, } criterion_main!(varuna_snark); diff --git a/algorithms/benches/snark/varuna_verifier.rs b/algorithms/benches/snark/varuna_verifier.rs new file mode 100644 index 0000000000..2df95980b6 --- /dev/null +++ b/algorithms/benches/snark/varuna_verifier.rs @@ -0,0 +1,187 @@ +// Copyright (c) 2019-2026 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Performs time measurements on the verification of test circuits with selected batch sizes and parameters. + - Generate artifacts with: + cargo bench --bench varuna_verifier --features test -- --generate + - Artifacts are ignored by git. To clean them, run: + cargo bench --bench varuna_verifier --features test -- --clean + - Obtain time measurements (using previously generated artifacts) with: + cargo bench --bench varuna_verifier --features test + The --serial feature can be added to deactivate parallelism. + - Flamegraph (on previously generated artifacts) with: + cargo flamegraph --bench varuna_verifier --features="test, serial" +*/ + +use snarkvm_algorithms::{ + AlgebraicSponge, + SNARK, + crypto_hash::PoseidonSponge, + snark::varuna::{ + CircuitVerifyingKey, + Proof, + TestCircuit, + VarunaHidingMode, + VarunaSNARK, + VarunaVersion, + ahp::AHPForR1CS, + }, +}; +use snarkvm_curves::bls12_377::{Bls12_377, Fq, Fr}; +use snarkvm_utilities::{CanonicalDeserialize, CanonicalSerialize, FromBytes, TestRng, ToBytes}; + +use std::{collections::BTreeMap, env, path::Path, time::Instant}; + +type VarunaInst = VarunaSNARK; +type FS = PoseidonSponge; + +fn main() { + /////////////////////////// User defined + + // How many times `verify_batch` runs when not using `--generate`. Larger values + // help flamegraph / timing stability. + let n_samples = 10; + + // Each tuple is: (batch_size, num_constraints, num_variables, + // num_public_inputs) + let batches = [(30, 50_000, 25_000, 64), (1, 5_000_000, 5_000_000, 1024)]; + + /////////////////////////// + + let generate = env::args().any(|arg| arg == "--generate"); + let clean = env::args().any(|arg| arg == "--clean"); + let artifact_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("benches/snark/varuna_verifier_artifacts"); + + if clean { + if generate { + panic!( + "--clean and --generate cannot be used together. Use --generate to generate\\ + the artifacts, --clean to delete them (and end), and neither to use the existing artifacts." + ); + } + std::fs::remove_dir_all(&artifact_path).unwrap(); + println!("Artifacts deleted."); + return; + } + + if !artifact_path.exists() { + if !generate { + panic!("--generate was not passed, but artifacts were not found."); + } + std::fs::create_dir(&artifact_path).unwrap(); + } + + let rng = &mut TestRng::default(); + + let max_vars = *batches.iter().map(|(_, _, num_variables, _)| num_variables).max().unwrap(); + let max_constraints = *batches.iter().map(|(_, num_constraints, _, _)| num_constraints).max().unwrap(); + let max_density = 2 * max_constraints; + + let max_degree = AHPForR1CS::::max_degree(max_constraints, max_vars, max_density).unwrap(); + let universal_srs = VarunaInst::universal_setup(max_degree).unwrap(); + let universal_prover = &universal_srs.to_universal_prover().unwrap(); + let universal_verifier = &universal_srs.to_universal_verifier().unwrap(); + let fs_parameters = FS::sample_parameters(); + + let varuna_version = VarunaVersion::V3; + + let batch_str = batches + .iter() + .map(|(batch_size, num_constraints, _, num_public_inputs)| { + format!("({batch_size} x [{num_public_inputs}, {num_constraints}])") + }) + .collect::>() + .join(" + "); + + println!("Batches: {batch_str}"); + + let sanitized_batch_str = batch_str.replace(' ', "_"); + + let vk_path = artifact_path.join(format!("vk_{sanitized_batch_str}.bin")); + let inputs_path = artifact_path.join(format!("inputs_{sanitized_batch_str}.bin")); + let proof_path = artifact_path.join(format!("proof_{sanitized_batch_str}.bin")); + + if generate { + println!("Generating artifacts for {batch_str}..."); + + let circuits_and_inputs: Vec<_> = batches + .iter() + .map(|&batch| { + let (batch_size, num_constraints, num_variables, num_public_inputs) = batch; + let (circuit, public_inputs) = + TestCircuit::gen_rand(num_public_inputs, num_constraints, num_variables, rng); + ( + VarunaInst::circuit_setup(&universal_srs, &circuit).unwrap(), + vec![circuit; batch_size], + vec![public_inputs; batch_size], + ) + }) + .collect(); + + let pks_to_circuits = circuits_and_inputs + .iter() + .map(|((pk, _), circuits, _)| (pk, circuits.as_slice())) + .collect::>(); + let vks_to_inputs = + circuits_and_inputs.iter().map(|((_, vk), _, inputs)| (vk, inputs.as_slice())).collect::>(); + + let proof = + VarunaInst::prove_batch(universal_prover, &fs_parameters, varuna_version, &pks_to_circuits, rng).unwrap(); + + let vks = vks_to_inputs.keys().map(|vk| (*vk).clone()).collect::>(); + let inputs = vks_to_inputs.values().cloned().collect::>(); + + let mut vk_buf = Vec::new(); + CanonicalSerialize::serialize_uncompressed(&vks, &mut vk_buf).unwrap(); + std::fs::write(&vk_path, vk_buf).expect("Failed to write verifying keys"); + let mut inputs_buf = Vec::new(); + CanonicalSerialize::serialize_uncompressed(&inputs, &mut inputs_buf).unwrap(); + std::fs::write(&inputs_path, inputs_buf).expect("Failed to write inputs"); + std::fs::write(&proof_path, proof.to_bytes_le().unwrap()).expect("Failed to write proof"); + } + + // Reload from disk so serialization and verification paths are exercised. + let vks: Vec> = CanonicalDeserialize::deserialize_uncompressed( + &*std::fs::read(&vk_path).expect("Failed to read verifying keys"), + ) + .unwrap(); + let inputs: Vec>> = + CanonicalDeserialize::deserialize_uncompressed(&*std::fs::read(&inputs_path).expect("Failed to read inputs")) + .unwrap(); + let proof = Proof::::read_le(&*std::fs::read(&proof_path).expect("Failed to read proof")).unwrap(); + let vks_to_inputs: BTreeMap<_, _> = vks.iter().zip(inputs.iter()).map(|(vk, inp)| (vk, inp.as_slice())).collect(); + + if generate { + println!("Verifying generated proof for {batch_str}..."); + assert!( + VarunaInst::verify_batch(universal_verifier, &fs_parameters, varuna_version, &vks_to_inputs, &proof) + .unwrap() + ); + println!("Verification successful"); + } else { + println!("Verifying proof for {batch_str} {n_samples} times..."); + let timer = Instant::now(); + for _ in 0..n_samples { + assert!( + VarunaInst::verify_batch(universal_verifier, &fs_parameters, varuna_version, &vks_to_inputs, &proof) + .unwrap() + ); + } + let elapsed = timer.elapsed().as_micros() as f64 / 1000.0; + let elapsed_avg = elapsed / n_samples as f64; + println!("Verification successful in {elapsed:.2} ms ({elapsed_avg:.2} ms per sample)"); + } +} diff --git a/algorithms/src/crypto_hash/poseidon.rs b/algorithms/src/crypto_hash/poseidon.rs index 48ed9bd071..d642c9bc66 100644 --- a/algorithms/src/crypto_hash/poseidon.rs +++ b/algorithms/src/crypto_hash/poseidon.rs @@ -219,14 +219,17 @@ impl PoseidonSponge { #[inline] fn apply_s_box(&mut self, is_full_round: bool) { + let pow_alpha_in_place: fn(&mut F) = match self.parameters.alpha { + 3 => pow_3_in_place, + 5 => pow_5_in_place, + 17 => pow_17_in_place, + alpha => panic!("No optimized S-box for alpha = {alpha}"), + }; + if is_full_round { - // Full rounds apply the S Box (x^alpha) to every element of state - for elem in self.state.iter_mut() { - *elem = elem.pow([self.parameters.alpha]); - } + self.state.iter_mut().for_each(pow_alpha_in_place); } else { - // Partial rounds apply the S Box (x^alpha) to just the first element of state - self.state[0] = self.state[0].pow([self.parameters.alpha]); + pow_alpha_in_place(&mut self.state[0]); } } @@ -501,3 +504,31 @@ impl PoseidonSponge { dest_elements } } + +// S-box functions to raise state elements to the values of alpha present in the +// codebase: 3, 5 and 17. These are more performant than the general pow +// function. +#[inline] +fn pow_3_in_place(val: &mut F) { + let val_copy = *val; + val.square_in_place(); + val.mul_assign(val_copy); +} + +#[inline] +fn pow_5_in_place(val: &mut F) { + let val_copy = *val; + val.square_in_place(); + val.square_in_place(); + val.mul_assign(val_copy); +} + +#[inline] +fn pow_17_in_place(val: &mut F) { + let val_copy = *val; + val.square_in_place(); + val.square_in_place(); + val.square_in_place(); + val.square_in_place(); + val.mul_assign(val_copy); +} diff --git a/algorithms/src/snark/varuna/ahp/prover/round_functions/third.rs b/algorithms/src/snark/varuna/ahp/prover/round_functions/third.rs index 8b0713c23e..d60b0178b3 100644 --- a/algorithms/src/snark/varuna/ahp/prover/round_functions/third.rs +++ b/algorithms/src/snark/varuna/ahp/prover/round_functions/third.rs @@ -116,7 +116,7 @@ impl AHPForR1CS { // Send the assigned matrix sums to the verifier only in VarunaVersion::V1. let msg = match varuna_version { VarunaVersion::V1 => Some(msg), - VarunaVersion::V2 => None, + VarunaVersion::V2 | VarunaVersion::V3 => None, }; let g_1 = DensePolynomial::from_coefficients_slice(&x_g_1_sum.coeffs[1..]); @@ -229,13 +229,15 @@ impl AHPForR1CS { Some(z_m_at_alpha), ) } - VarunaVersion::V2 => Self::calculate_lineval_sumcheck_instance_witness_polys( - label, - variable_domain, - max_variable_domain, - combiner, - z_m_at_alpha, - ), + VarunaVersion::V2 | VarunaVersion::V3 => { + Self::calculate_lineval_sumcheck_instance_witness_polys( + label, + variable_domain, + max_variable_domain, + combiner, + z_m_at_alpha, + ) + } }); } } diff --git a/algorithms/src/snark/varuna/ahp/verifier/messages.rs b/algorithms/src/snark/varuna/ahp/verifier/messages.rs index 4a0ea0f644..55c2fc144a 100644 --- a/algorithms/src/snark/varuna/ahp/verifier/messages.rs +++ b/algorithms/src/snark/varuna/ahp/verifier/messages.rs @@ -177,7 +177,7 @@ pub fn select_third_round_challenges( } Ok((*alpha, first_round_batch_combiners.clone(), *eta_b, *eta_c)) } - VarunaVersion::V2 => { + VarunaVersion::V2 | VarunaVersion::V3 => { let SecondMessage { alpha, eta_b, eta_c } = verifier_second_message; if eta_b.is_some() || eta_c.is_some() { return Err(anyhow::anyhow!( diff --git a/algorithms/src/snark/varuna/ahp/verifier/verifier.rs b/algorithms/src/snark/varuna/ahp/verifier/verifier.rs index b37d45bd50..c53dd477e1 100644 --- a/algorithms/src/snark/varuna/ahp/verifier/verifier.rs +++ b/algorithms/src/snark/varuna/ahp/verifier/verifier.rs @@ -164,7 +164,7 @@ impl AHPForR1CS { let [alpha, eta_b, eta_c]: [_; 3] = first.try_into().map_err(anyhow::Error::msg)?; (alpha, Some(eta_b), Some(eta_c)) } - VarunaVersion::V2 => { + VarunaVersion::V2 | VarunaVersion::V3 => { let elems = fs_rng.squeeze_nonnative_field_elements(1); let alpha = elems[0]; (alpha, None, None) diff --git a/algorithms/src/snark/varuna/data_structures/proof.rs b/algorithms/src/snark/varuna/data_structures/proof.rs index 68565e5ff2..3bbe96ee5e 100644 --- a/algorithms/src/snark/varuna/data_structures/proof.rs +++ b/algorithms/src/snark/varuna/data_structures/proof.rs @@ -409,7 +409,7 @@ pub fn proof_size( match varuna_version { VarunaVersion::V1 => Err(anyhow!("Proof-size calculation not implemented for Varuna version V1")), - VarunaVersion::V2 => { + VarunaVersion::V2 | VarunaVersion::V3 => { // All fields are serialised in Compressed mode The breakdown is as // follows: // - batch sizes: one `usize` (which is serialised as a `u64`) for each batch diff --git a/algorithms/src/snark/varuna/mode.rs b/algorithms/src/snark/varuna/mode.rs index 16dea13aa9..aa05b6a415 100644 --- a/algorithms/src/snark/varuna/mode.rs +++ b/algorithms/src/snark/varuna/mode.rs @@ -44,6 +44,7 @@ impl SNARKMode for VarunaNonHidingMode { pub enum VarunaVersion { V1 = 1, V2 = 2, + V3 = 3, } impl ToBytes for VarunaVersion { @@ -58,6 +59,7 @@ impl FromBytes for VarunaVersion { 0 => Err(io_error("Zero is not a valid Varuna version")), 1 => Ok(Self::V1), 2 => Ok(Self::V2), + 3 => Ok(Self::V3), _ => Err(io_error("Invalid Varuna version")), } } diff --git a/algorithms/src/snark/varuna/tests.rs b/algorithms/src/snark/varuna/tests.rs index d70c5230b3..6a24f9f1da 100644 --- a/algorithms/src/snark/varuna/tests.rs +++ b/algorithms/src/snark/varuna/tests.rs @@ -59,7 +59,8 @@ mod varuna { let wrong_varuna_version = match varuna_version { VarunaVersion::V1 => VarunaVersion::V2, - VarunaVersion::V2 => VarunaVersion::V1, + VarunaVersion::V2 => VarunaVersion::V3, + VarunaVersion::V3 => VarunaVersion::V1, }; for i in 0..5 { @@ -387,7 +388,8 @@ mod varuna_hiding { let wrong_varuna_version = match varuna_version { VarunaVersion::V1 => VarunaVersion::V2, - VarunaVersion::V2 => VarunaVersion::V1, + VarunaVersion::V2 => VarunaVersion::V3, + VarunaVersion::V3 => VarunaVersion::V1, }; for _ in 0..num_times { @@ -571,10 +573,11 @@ mod varuna_hiding { let universal_prover = &universal_srs.to_universal_prover().unwrap(); let universal_verifier = &universal_srs.to_universal_verifier().unwrap(); let fs_parameters = FS::sample_parameters(); - for varuna_version in [VarunaVersion::V1, VarunaVersion::V2] { + for varuna_version in [VarunaVersion::V1, VarunaVersion::V2, VarunaVersion::V3] { let wrong_varuna_version = match varuna_version { VarunaVersion::V1 => VarunaVersion::V2, - VarunaVersion::V2 => VarunaVersion::V1, + VarunaVersion::V2 => VarunaVersion::V3, + VarunaVersion::V3 => VarunaVersion::V1, }; let (index_pk, index_vk) = VarunaInst::circuit_setup(&universal_srs, &circuit).unwrap(); println!("Called circuit setup"); diff --git a/algorithms/src/snark/varuna/varuna.rs b/algorithms/src/snark/varuna/varuna.rs index aad64c8626..acfaa599de 100644 --- a/algorithms/src/snark/varuna/varuna.rs +++ b/algorithms/src/snark/varuna/varuna.rs @@ -43,6 +43,7 @@ use crate::{ }, srs::UniversalVerifier, }; +use sha2::{Digest, Sha256}; use snarkvm_curves::PairingEngine; use snarkvm_fields::{One, PrimeField, ToConstraintField, Zero}; use snarkvm_utilities::{ToBytes, dev_eprintln, dev_println, to_bytes_le}; @@ -137,19 +138,52 @@ impl, SM: SNARKMode> VarunaSNARK fs_parameters: &FS::Parameters, inputs_and_batch_sizes: &BTreeMap])>, circuit_commitments: impl Iterator]>, + varuna_version: VarunaVersion, ) -> FS { - let mut sponge = FS::new_with_parameters(fs_parameters); - sponge.absorb_bytes(Self::PROTOCOL_NAME); - for (batch_size, inputs) in inputs_and_batch_sizes.values() { - sponge.absorb_bytes(&(*batch_size as u64).to_le_bytes()); - for input in inputs.iter() { - sponge.absorb_nonnative_field_elements(input.iter().copied()); + match varuna_version { + // Before version V3, we absorb the public parameters and public + // inputs into the sponge directly + VarunaVersion::V1 | VarunaVersion::V2 => { + let mut sponge = FS::new_with_parameters(fs_parameters); + + sponge.absorb_bytes(Self::PROTOCOL_NAME); + for (batch_size, inputs) in inputs_and_batch_sizes.values() { + sponge.absorb_bytes(&(*batch_size as u64).to_le_bytes()); + for input in inputs.iter() { + sponge.absorb_nonnative_field_elements(input.iter().copied()); + } + } + for circuit_specific_commitments in circuit_commitments { + sponge.absorb_native_field_elements(circuit_specific_commitments); + } + sponge + } + // Starting with version V3, we digest the values into a single + // field element using SHA-256 and then absorb that element into the + // sponge + VarunaVersion::V3 => { + let mut sponge = FS::new_with_parameters(fs_parameters); + + let mut digest = Sha256::new(); + + digest.update(Self::PROTOCOL_NAME); + for (batch_size, inputs) in inputs_and_batch_sizes.values() { + digest.update((*batch_size as u64).to_le_bytes()); + for input in inputs.iter() { + for val in input.to_bytes_le().unwrap() { + digest.update([val]); + } + } + } + for circuit_specific_commitments in circuit_commitments { + digest.update(circuit_specific_commitments.to_bytes_le().unwrap()); + } + + sponge.absorb_bytes(&digest.finalize()); + + sponge } } - for circuit_specific_commitments in circuit_commitments { - sponge.absorb_native_field_elements(circuit_specific_commitments); - } - sponge } fn init_sponge_for_certificate( @@ -429,7 +463,8 @@ where let circuit_commitments = keys_to_constraints.keys().map(|pk| pk.circuit_verifying_key.circuit_commitments.as_slice()); dev_println!("inputs_and_batch_sizes: {inputs_and_batch_sizes:?}"); - let mut sponge = Self::init_sponge(fs_parameters, &inputs_and_batch_sizes, circuit_commitments.clone()); + let mut sponge = + Self::init_sponge(fs_parameters, &inputs_and_batch_sizes, circuit_commitments.clone(), varuna_version); // -------------------------------------------------------------------- // First round @@ -487,7 +522,7 @@ where let (prover_prepare_third_message, prover_state, verifier_prepare_third_msg, verifier_state) = { match varuna_version { VarunaVersion::V1 => (None, prover_state, None, verifier_state), - VarunaVersion::V2 => { + VarunaVersion::V2 | VarunaVersion::V3 => { let (prover_prepare_third_message, prover_state) = AHPForR1CS::<_, SM>::prover_prepare_third_round( &verifier_first_message, &verifier_second_msg, @@ -549,7 +584,7 @@ where &mut sponge, ); } - VarunaVersion::V2 => { + VarunaVersion::V2 | VarunaVersion::V3 => { if prover_third_message.is_some() { return Err(anyhow!("Expected prover to not contribute sums in the third round."))?; } @@ -560,7 +595,7 @@ where // Extract the prover's third message to be used in the verifier's third round. let prover_third_message = match varuna_version { VarunaVersion::V1 => prover_third_message, - VarunaVersion::V2 => prover_prepare_third_message, + VarunaVersion::V2 | VarunaVersion::V3 => prover_prepare_third_message, } .ok_or_else(|| anyhow!("Prover did not contribute sums in the expected round."))?; @@ -944,7 +979,8 @@ where let circuit_commitments = keys_to_inputs.keys().map(|vk| vk.circuit_commitments.as_slice()); dev_println!("inputs_and_batch_sizes: {inputs_and_batch_sizes:?}"); - let mut sponge = Self::init_sponge(fs_parameters, &inputs_and_batch_sizes, circuit_commitments.clone()); + let mut sponge = + Self::init_sponge(fs_parameters, &inputs_and_batch_sizes, circuit_commitments.clone(), varuna_version); // -------------------------------------------------------------------- // First round @@ -975,7 +1011,7 @@ where let verifier_state = { match varuna_version { VarunaVersion::V1 => verifier_state, - VarunaVersion::V2 => { + VarunaVersion::V2 | VarunaVersion::V3 => { let prepare_third_round_time = start_timer!(|| "Prep third round"); Self::absorb_sums(&proof.third_msg.sums.clone().into_iter().flatten().collect_vec(), &mut sponge); let (_, verifier_state) = AHPForR1CS::<_, SM>::verifier_prepare_third_round( @@ -1002,7 +1038,7 @@ where &mut sponge, ); } - VarunaVersion::V2 => { + VarunaVersion::V2 | VarunaVersion::V3 => { Self::absorb_labeled(&third_commitments, &mut sponge); } } diff --git a/console/Cargo.toml b/console/Cargo.toml index 793cfec4c0..3197730ae4 100644 --- a/console/Cargo.toml +++ b/console/Cargo.toml @@ -55,8 +55,13 @@ filesystem = [ "snarkvm-console-network/filesystem" ] locktick = [ "snarkvm-console-collections?/locktick" ] network = [ "collections", "snarkvm-console-network" ] program = [ "network", "snarkvm-console-program" ] -serial = [ "snarkvm-console-collections/serial" ] +serial = [ "snarkvm-console-algorithms/serial", "snarkvm-console-collections/serial" ] types = [ "snarkvm-console-types" ] test_targets = [ "snarkvm-console-network/test_targets" ] test_consensus_heights = [ "snarkvm-console-network/test_consensus_heights" ] +dev-print = [ + "snarkvm-console-algorithms?/dev-print", + "snarkvm-console-network?/dev-print", + "snarkvm-console-program?/dev-print" +] dev_skip_checks = [ ] diff --git a/console/algorithms/Cargo.toml b/console/algorithms/Cargo.toml index 0e6a2eee75..c4757f6cf5 100644 --- a/console/algorithms/Cargo.toml +++ b/console/algorithms/Cargo.toml @@ -49,6 +49,9 @@ version = "1.0" [dependencies.hex] version = "0.4" +[dependencies.rayon] +workspace = true + [dependencies.k256] workspace = true @@ -85,3 +88,4 @@ features = [ "preserve_order" ] [features] dev-print = [ "snarkvm-utilities/dev-print" ] +serial = [ ] diff --git a/console/algorithms/src/bhp/hasher/hash_uncompressed.rs b/console/algorithms/src/bhp/hasher/hash_uncompressed.rs index d882330cdc..6a0529a7e1 100644 --- a/console/algorithms/src/bhp/hasher/hash_uncompressed.rs +++ b/console/algorithms/src/bhp/hasher/hash_uncompressed.rs @@ -29,7 +29,12 @@ impl HashUncompres /// the BHP commitment scheme, as it is typically not used by applications. fn hash_uncompressed(&self, input: &[Self::Input]) -> Result { // Ensure the input size is at least the window size. - ensure!(input.len() > Self::MIN_BITS, "Inputs to this BHP must be greater than {} bits", Self::MIN_BITS); + ensure!( + input.len() > Self::MIN_BITS, + "Inputs to this BHP must be greater than {} bits (window size: {WINDOW_SIZE}, num windows: {NUM_WINDOWS}), actual bits: {}", + Self::MIN_BITS, + input.len() + ); // Ensure the input size is within the parameter size, ensure!( input.len() <= Self::MAX_BITS, @@ -49,19 +54,49 @@ impl HashUncompres Cow::Borrowed(input) }; - // Compute sum of h_i^{sum of (1-2*c_{i,j,2})*(1+c_{i,j,0}+2*c_{i,j,1})*2^{4*(j-1)} for all j in segment} - // for all i. Described in section 5.4.1.7 in the Zcash protocol specification. - // - // Note: `.zip()` is used here (as opposed to `.zip_eq()`) as the input can be less than - // `NUM_WINDOWS * WINDOW_SIZE * BHP_CHUNK_SIZE` in length, which is the parameter size here. - Ok(input + let sum = input .chunks(WINDOW_SIZE as usize * BHP_CHUNK_SIZE) - .zip(&*self.bases_lookup) - .flat_map(|(bits, bases)| { - bits.chunks(BHP_CHUNK_SIZE).zip(bases).map(|(chunk_bits, base)| { - base[(chunk_bits[0] as usize) | (chunk_bits[1] as usize) << 1 | (chunk_bits[2] as usize) << 2] - }) + .zip(self.combined_bases_lookup.iter()) + .zip(self.bases_lookup.iter()) + .flat_map(|((window_bits, combined_bases), bases)| { + // The number of full BHP_CHUNK_SIZE-bit chunks in the window. + // The preprocessed points corresponding to these will be looked + // up in combined_bases. + let num_combined_bases = window_bits.len() / (BHP_CHUNK_SIZE * BHP_NUM_COMBINED_CHUNKS); + // The number of bits in the window belonging to full chunks. + let num_combined_bits = num_combined_bases * BHP_CHUNK_SIZE * BHP_NUM_COMBINED_CHUNKS; + + let combined = window_bits[..num_combined_bits] + .chunks_exact(BHP_CHUNK_SIZE * BHP_NUM_COMBINED_CHUNKS) + .zip(combined_bases) + .map(|(combined_chunks_bits, combined_base)| { + // Reconstruct the index as the integer represented by the bits of the combined chunks + // in a suitable order. + let index = combined_chunks_bits.chunks_exact(BHP_CHUNK_SIZE).fold(0, |idx, chunk_bits| { + (idx << BHP_CHUNK_SIZE) + | (chunk_bits[0] as usize) + | (chunk_bits[1] as usize) << 1 + | (chunk_bits[2] as usize) << 2 + }); + + combined_base[index] + }); + + // Trailing bits outside the last BHP_NUM_COMBINED_CHUNKS-chunk + // result in lookups `bases` table. + let base_offset = num_combined_bases * BHP_NUM_COMBINED_CHUNKS; + let trailing = &window_bits[num_combined_bits..]; + let remainder = + trailing.chunks_exact(BHP_CHUNK_SIZE).enumerate().map(move |(triplet_index, chunk_bits)| { + let idx = + (chunk_bits[0] as usize) | (chunk_bits[1] as usize) << 1 | (chunk_bits[2] as usize) << 2; + bases[base_offset + triplet_index][idx] + }); + + combined.chain(remainder) }) - .sum()) + .sum(); + + Ok(sum) } } diff --git a/console/algorithms/src/bhp/hasher/mod.rs b/console/algorithms/src/bhp/hasher/mod.rs index 905d0abd32..3282097a95 100644 --- a/console/algorithms/src/bhp/hasher/mod.rs +++ b/console/algorithms/src/bhp/hasher/mod.rs @@ -15,10 +15,15 @@ mod hash_uncompressed; +#[cfg(test)] +mod tests; + use crate::Blake2Xs; use snarkvm_console_types::prelude::*; use snarkvm_utilities::BigInteger; +#[cfg(not(feature = "serial"))] +use rayon::prelude::*; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -26,6 +31,9 @@ use std::sync::Arc; pub(super) const BHP_CHUNK_SIZE: usize = 3; pub(super) const BHP_LOOKUP_SIZE: usize = 1 << BHP_CHUNK_SIZE; +// The amount of chunks (i.e. bit triplets) to preprocess together in the lookup table. +pub(super) const BHP_NUM_COMBINED_CHUNKS: usize = 4; + /// BHP is a collision-resistant hash function that takes a variable-length input. /// The BHP hasher is used to process one internal iteration of the BHP hash function. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -35,6 +43,11 @@ pub struct BHPHasher>>>, /// The bases lookup table for the BHP hash. bases_lookup: Arc; BHP_LOOKUP_SIZE]>>>, + /// The preprocessed combinations of elements in the bases lookup table. + // For each group of BHP_NUM_COMBINED_CHUNKS contiguous + // BHP_LOOKUP_SIZE-element tuples in `bases_lookup`, this contains + // all possible BHP_LOOKUP_SIZE^BHP_NUM_COMBINED_CHUNKS cross-tuple sums. + combined_bases_lookup: Arc>>>>, /// The random base for the BHP commitment. random_base: Arc>>, } @@ -47,6 +60,9 @@ impl BHPHasher Result { + #[cfg(feature = "dev-print")] + let timer = std::time::Instant::now(); + // Calculate the maximum window size. let mut maximum_window_size = 0; let mut range = E::BigInteger::from(2_u64); @@ -82,10 +98,10 @@ impl BHPHasher::zero(); BHP_LOOKUP_SIZE]; for (i, element) in lookup.iter_mut().enumerate().take(BHP_LOOKUP_SIZE) { @@ -110,6 +126,35 @@ impl BHPHasher::zero()], |prev_sums, new_terms| { + prev_sums + .iter() + .flat_map(|prev_sum| new_terms.iter().map(|new_term| *prev_sum + *new_term)) + .collect::>>() + }) + }) + .collect::>() + }) + .collect::>>(); + // Next, compute the random base. let (generator, _, _) = Blake2Xs::hash_to_curve::(&format!("Aleo.BHP.{NUM_WINDOWS}.{WINDOW_SIZE}.{domain}.Randomizer")); @@ -125,7 +170,46 @@ impl BHPHasher()).sum(); + let num_els_combined_bases_lookup: usize = + combined_bases_lookup.iter().map(|vs| vs.iter().map(|v| v.len()).sum::()).sum(); + let num_els_random_base = random_base.len(); + + let bytes = (num_els_bases + num_els_bases_lookup + num_els_combined_bases_lookup + num_els_random_base) + * std::mem::size_of::>(); + + println!(" Approximate BHP hasher size: {bytes} B"); + } + + Ok(Self { + bases: Arc::new(bases), + bases_lookup: Arc::new(bases_lookup), + combined_bases_lookup: Arc::new(combined_bases_lookup), + random_base: Arc::new(random_base), + }) } /// Returns the bases. diff --git a/console/algorithms/src/bhp/hasher/tests.rs b/console/algorithms/src/bhp/hasher/tests.rs new file mode 100644 index 0000000000..29cecfccbb --- /dev/null +++ b/console/algorithms/src/bhp/hasher/tests.rs @@ -0,0 +1,318 @@ +// Copyright (c) 2019-2026 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use snarkvm_console_types::environment::Console; +use snarkvm_utilities::ToBytes; + +// Test vectors. For each BHP{256, 512, 768, 1024}, we test the output on +// deterministic inputs of certain representative lengths. The second element of +// each pair is the x-coordinate of the hash. + +// TODO (Antonio) specify +// Display new expectations with: +// cargo test -p snarkvm-console-algorithms print_bhp_expectations -- --ignored --nocapture + +static EXPECTATIONS_256: [(usize, [u8; 32]); 10] = [ + (172, [ + 0x96, 0xcb, 0x38, 0x0e, 0x35, 0xac, 0xbe, 0xb0, 0xed, 0x80, 0x8b, 0x77, 0x5f, 0x80, 0xfe, 0x1b, 0x24, 0x06, + 0x96, 0x0d, 0xab, 0x26, 0xbb, 0xf2, 0xbf, 0x70, 0x9d, 0xe1, 0xba, 0x0e, 0x36, 0x0e, + ]), + (173, [ + 0x96, 0xcb, 0x38, 0x0e, 0x35, 0xac, 0xbe, 0xb0, 0xed, 0x80, 0x8b, 0x77, 0x5f, 0x80, 0xfe, 0x1b, 0x24, 0x06, + 0x96, 0x0d, 0xab, 0x26, 0xbb, 0xf2, 0xbf, 0x70, 0x9d, 0xe1, 0xba, 0x0e, 0x36, 0x0e, + ]), + (174, [ + 0x96, 0xcb, 0x38, 0x0e, 0x35, 0xac, 0xbe, 0xb0, 0xed, 0x80, 0x8b, 0x77, 0x5f, 0x80, 0xfe, 0x1b, 0x24, 0x06, + 0x96, 0x0d, 0xab, 0x26, 0xbb, 0xf2, 0xbf, 0x70, 0x9d, 0xe1, 0xba, 0x0e, 0x36, 0x0e, + ]), + (175, [ + 0xcb, 0x9a, 0x09, 0xd9, 0xe7, 0xa3, 0xa4, 0x2d, 0x5e, 0x25, 0x75, 0x8f, 0xe9, 0x25, 0x38, 0x5f, 0x7a, 0x9e, + 0xf4, 0x46, 0x15, 0xfc, 0xd5, 0x55, 0x70, 0x50, 0x35, 0xf9, 0x3e, 0xfe, 0xd0, 0x0b, + ]), + (176, [ + 0xaf, 0xee, 0x41, 0x1f, 0x7e, 0xd4, 0x0a, 0x09, 0x22, 0x74, 0x7c, 0xc4, 0xe3, 0x7d, 0x0f, 0xfc, 0x40, 0x3f, + 0xb5, 0xeb, 0xb2, 0xe7, 0x76, 0x7f, 0xfe, 0x58, 0x77, 0x7e, 0x3c, 0x09, 0x9b, 0x06, + ]), + (342, [ + 0x13, 0x1c, 0x71, 0x01, 0x4b, 0x80, 0xae, 0x6f, 0x6b, 0x34, 0xc5, 0x8a, 0xc0, 0x17, 0xd4, 0x39, 0x69, 0xa5, + 0x42, 0xf3, 0xff, 0x3f, 0x2e, 0xda, 0x7b, 0x6d, 0x4e, 0x3e, 0xd2, 0xee, 0x70, 0x06, + ]), + (507, [ + 0x79, 0xbb, 0xc2, 0x18, 0xcb, 0x0d, 0xfb, 0xc5, 0xc6, 0xf0, 0x38, 0x95, 0xf0, 0x84, 0x38, 0x82, 0xc8, 0xc7, + 0xe3, 0x22, 0x62, 0xbd, 0x44, 0x38, 0x3a, 0xaf, 0x3d, 0xa2, 0x60, 0x11, 0x2b, 0x12, + ]), + (510, [ + 0x3b, 0x55, 0xe8, 0x4a, 0x83, 0xf2, 0x28, 0xd7, 0x3e, 0xa4, 0x4f, 0x37, 0xf0, 0x2b, 0x40, 0xdc, 0x6a, 0x14, + 0xb2, 0x06, 0xce, 0xc0, 0xba, 0x95, 0x4a, 0x41, 0x94, 0xc8, 0x63, 0x84, 0xec, 0x09, + ]), + (512, [ + 0x4b, 0xc1, 0xc5, 0xa1, 0xa2, 0x23, 0xdf, 0xc5, 0x0d, 0x75, 0x7a, 0x88, 0x4b, 0x09, 0x3a, 0x28, 0xbd, 0x19, + 0x94, 0x1e, 0x8a, 0x1a, 0xba, 0xd0, 0xf1, 0x6b, 0xff, 0x46, 0x96, 0x5d, 0x0c, 0x07, + ]), + (513, [ + 0xab, 0xd7, 0xa7, 0x84, 0x19, 0x2f, 0x41, 0xb3, 0x9b, 0x70, 0x85, 0x3b, 0x27, 0x8e, 0x92, 0xbf, 0x1e, 0x91, + 0x8d, 0xd0, 0xcd, 0x4e, 0xf7, 0xd0, 0x65, 0x32, 0xe3, 0x96, 0xb0, 0x31, 0xb3, 0x00, + ]), +]; + +static EXPECTATIONS_512: [(usize, [u8; 32]); 11] = [ + (130, [ + 0xbf, 0xeb, 0x52, 0xa0, 0x3a, 0xcb, 0x3f, 0x38, 0x58, 0xd0, 0x1a, 0x8d, 0x24, 0x5f, 0x70, 0xf0, 0xce, 0x34, + 0x98, 0xf0, 0x61, 0x63, 0xda, 0x84, 0x0b, 0xc8, 0x4b, 0x5d, 0xe7, 0x4d, 0x0b, 0x10, + ]), + (131, [ + 0xbf, 0xeb, 0x52, 0xa0, 0x3a, 0xcb, 0x3f, 0x38, 0x58, 0xd0, 0x1a, 0x8d, 0x24, 0x5f, 0x70, 0xf0, 0xce, 0x34, + 0x98, 0xf0, 0x61, 0x63, 0xda, 0x84, 0x0b, 0xc8, 0x4b, 0x5d, 0xe7, 0x4d, 0x0b, 0x10, + ]), + (132, [ + 0xbf, 0xeb, 0x52, 0xa0, 0x3a, 0xcb, 0x3f, 0x38, 0x58, 0xd0, 0x1a, 0x8d, 0x24, 0x5f, 0x70, 0xf0, 0xce, 0x34, + 0x98, 0xf0, 0x61, 0x63, 0xda, 0x84, 0x0b, 0xc8, 0x4b, 0x5d, 0xe7, 0x4d, 0x0b, 0x10, + ]), + (133, [ + 0x42, 0x50, 0x5a, 0x03, 0x91, 0x44, 0x9a, 0x39, 0xac, 0xbb, 0x1d, 0xf4, 0x71, 0x7b, 0xed, 0xff, 0x0e, 0x05, + 0x87, 0x6b, 0x52, 0x22, 0x83, 0x35, 0x7c, 0x6f, 0xfd, 0x41, 0xa6, 0xa4, 0x1f, 0x0e, + ]), + (134, [ + 0xab, 0x66, 0xd7, 0xba, 0x51, 0x8c, 0x0c, 0x55, 0xb0, 0x8e, 0x78, 0x48, 0x04, 0xe4, 0x5e, 0xda, 0xf3, 0x41, + 0xed, 0x4d, 0x2a, 0xd5, 0x3f, 0xec, 0x80, 0x2d, 0x8f, 0x11, 0x07, 0x93, 0x42, 0x0d, + ]), + (450, [ + 0x33, 0xc1, 0xff, 0xb3, 0x2a, 0xb0, 0xfd, 0x6e, 0xd3, 0x10, 0xee, 0xbb, 0x7d, 0x26, 0x20, 0xc8, 0x39, 0xfc, + 0x00, 0x35, 0xd3, 0x77, 0x67, 0xa2, 0x2e, 0x90, 0xb2, 0xea, 0x7b, 0x76, 0x62, 0x0e, + ]), + (452, [ + 0x8f, 0xf3, 0x9e, 0xc3, 0x53, 0x6a, 0x76, 0x0a, 0x3b, 0x8d, 0x32, 0x29, 0x83, 0x69, 0x7f, 0x7c, 0x89, 0x36, + 0xb6, 0xe8, 0x28, 0x60, 0x1f, 0x0b, 0xa6, 0x75, 0x3b, 0xdf, 0x7f, 0x2f, 0xed, 0x11, + ]), + (768, [ + 0x9b, 0x07, 0x98, 0x7b, 0x7e, 0xe4, 0xe6, 0x4b, 0xa2, 0xab, 0x8a, 0x0e, 0x77, 0x8f, 0x08, 0x53, 0x3d, 0x9d, + 0x6c, 0x3e, 0xdf, 0x2f, 0x6d, 0x4e, 0xf0, 0xee, 0x28, 0xd3, 0xb9, 0x8c, 0x7e, 0x03, + ]), + (771, [ + 0xc2, 0x2e, 0x61, 0x41, 0xec, 0x3a, 0xb4, 0x37, 0x3a, 0xfc, 0xbb, 0x76, 0xeb, 0xa2, 0x82, 0x5c, 0xa4, 0x65, + 0x0d, 0xfb, 0x3d, 0xd5, 0x11, 0x65, 0x6d, 0x7e, 0xc1, 0x11, 0x19, 0xd2, 0x5d, 0x00, + ]), + (773, [ + 0xac, 0xc3, 0xad, 0x40, 0xbb, 0x69, 0x73, 0x08, 0xa5, 0x24, 0xe0, 0xa0, 0xfc, 0xf8, 0x36, 0x0b, 0x56, 0xb2, + 0xe9, 0xc6, 0x42, 0xbb, 0x07, 0x7a, 0x52, 0xc2, 0x51, 0xbd, 0xbf, 0x8d, 0x6d, 0x0c, + ]), + (774, [ + 0x7a, 0x3b, 0x4d, 0xb5, 0x7e, 0x47, 0xe4, 0xe0, 0x15, 0x4b, 0x8d, 0x31, 0x3f, 0x25, 0x7c, 0x79, 0x6d, 0xa5, + 0x6a, 0xf3, 0x8d, 0x6f, 0x99, 0x8f, 0xda, 0x85, 0x3e, 0xad, 0x26, 0x8a, 0x33, 0x12, + ]), +]; + +static EXPECTATIONS_768: [(usize, [u8; 32]); 10] = [ + (70, [ + 0xb3, 0x54, 0x5e, 0x94, 0x56, 0x61, 0xf2, 0xfd, 0xc2, 0xd9, 0x1d, 0x1e, 0x1b, 0xb7, 0xec, 0xb4, 0x75, 0x6d, + 0xb4, 0x36, 0x93, 0x77, 0xc1, 0x81, 0xb5, 0xe1, 0x9f, 0xaa, 0xf0, 0xa4, 0xb2, 0x09, + ]), + (71, [ + 0xb3, 0x54, 0x5e, 0x94, 0x56, 0x61, 0xf2, 0xfd, 0xc2, 0xd9, 0x1d, 0x1e, 0x1b, 0xb7, 0xec, 0xb4, 0x75, 0x6d, + 0xb4, 0x36, 0x93, 0x77, 0xc1, 0x81, 0xb5, 0xe1, 0x9f, 0xaa, 0xf0, 0xa4, 0xb2, 0x09, + ]), + (72, [ + 0xb3, 0x54, 0x5e, 0x94, 0x56, 0x61, 0xf2, 0xfd, 0xc2, 0xd9, 0x1d, 0x1e, 0x1b, 0xb7, 0xec, 0xb4, 0x75, 0x6d, + 0xb4, 0x36, 0x93, 0x77, 0xc1, 0x81, 0xb5, 0xe1, 0x9f, 0xaa, 0xf0, 0xa4, 0xb2, 0x09, + ]), + (73, [ + 0x4e, 0x39, 0xe2, 0xb6, 0x0b, 0x83, 0x05, 0x83, 0x76, 0x89, 0x1f, 0xee, 0x4d, 0x78, 0x10, 0x71, 0xb6, 0x44, + 0x6d, 0xc3, 0x5b, 0x99, 0x94, 0x1c, 0x20, 0xf3, 0x46, 0x75, 0x2d, 0x7b, 0xdf, 0x0a, + ]), + (74, [ + 0x19, 0xb5, 0x3e, 0x97, 0x21, 0x8b, 0x09, 0xfd, 0x9b, 0xdc, 0x2c, 0xb3, 0xc2, 0xe8, 0x68, 0x13, 0x28, 0x07, + 0x62, 0x7e, 0xd4, 0x88, 0xb9, 0x9f, 0xb8, 0x36, 0xa4, 0x97, 0x77, 0x29, 0xa4, 0x01, + ]), + (552, [ + 0xf5, 0x86, 0x95, 0x07, 0x77, 0x77, 0x98, 0x1d, 0x1b, 0x21, 0x99, 0xf6, 0xa3, 0x05, 0x2f, 0xc0, 0xb5, 0x4d, + 0x4b, 0x47, 0x77, 0x43, 0x9a, 0xe2, 0x15, 0xb9, 0x65, 0xab, 0x0a, 0x3a, 0xfe, 0x0d, + ]), + (1029, [ + 0x51, 0x2b, 0x29, 0xa1, 0x7e, 0x70, 0xc1, 0x10, 0x2c, 0xd8, 0x25, 0xbf, 0x1e, 0xb5, 0x6d, 0x52, 0x27, 0xfe, + 0x38, 0xa5, 0xbb, 0x56, 0x91, 0x0f, 0xe9, 0xb1, 0x60, 0xcd, 0x83, 0x60, 0x6f, 0x02, + ]), + (1032, [ + 0x6a, 0xbd, 0x2e, 0xf7, 0x0f, 0xd1, 0x46, 0x03, 0x40, 0x02, 0x0d, 0xd6, 0x81, 0xdb, 0x0c, 0x2f, 0x88, 0x90, + 0x29, 0xef, 0x81, 0xd2, 0x29, 0x29, 0x7b, 0x84, 0xde, 0xd8, 0xaf, 0x2b, 0x57, 0x0f, + ]), + (1034, [ + 0x18, 0xf1, 0x7f, 0xfe, 0x3c, 0x2e, 0xdf, 0x47, 0xda, 0xb3, 0x8f, 0x6c, 0x17, 0x32, 0x8c, 0x77, 0xc7, 0x73, + 0xe9, 0xaa, 0xfd, 0x6e, 0x7a, 0x55, 0xe5, 0x12, 0x20, 0x9b, 0xc0, 0xa8, 0x33, 0x06, + ]), + (1035, [ + 0x18, 0xf1, 0x7f, 0xfe, 0x3c, 0x2e, 0xdf, 0x47, 0xda, 0xb3, 0x8f, 0x6c, 0x17, 0x32, 0x8c, 0x77, 0xc7, 0x73, + 0xe9, 0xaa, 0xfd, 0x6e, 0x7a, 0x55, 0xe5, 0x12, 0x20, 0x9b, 0xc0, 0xa8, 0x33, 0x06, + ]), +]; + +static EXPECTATIONS_1024: [(usize, [u8; 32]); 10] = [ + (163, [ + 0x7d, 0x7e, 0x51, 0x79, 0x94, 0xe8, 0xcc, 0x2a, 0x83, 0x04, 0x06, 0xb2, 0x0c, 0xc0, 0xd5, 0xb2, 0x3c, 0x77, + 0x5e, 0xe4, 0x3f, 0x06, 0x06, 0x08, 0xff, 0xa8, 0x6f, 0x23, 0xdf, 0x48, 0xdc, 0x0f, + ]), + (164, [ + 0xc3, 0x0a, 0xdf, 0x59, 0x9d, 0x89, 0x45, 0x39, 0x7a, 0x21, 0x01, 0xc3, 0x1f, 0xd9, 0xcc, 0xac, 0x0c, 0x38, + 0xf3, 0x76, 0x93, 0xc7, 0x30, 0xb9, 0x67, 0x65, 0xb8, 0x42, 0x25, 0x82, 0x41, 0x0b, + ]), + (165, [ + 0xc3, 0x0a, 0xdf, 0x59, 0x9d, 0x89, 0x45, 0x39, 0x7a, 0x21, 0x01, 0xc3, 0x1f, 0xd9, 0xcc, 0xac, 0x0c, 0x38, + 0xf3, 0x76, 0x93, 0xc7, 0x30, 0xb9, 0x67, 0x65, 0xb8, 0x42, 0x25, 0x82, 0x41, 0x0b, + ]), + (166, [ + 0xdd, 0x6a, 0x1e, 0x62, 0x2b, 0x85, 0x93, 0x42, 0xbd, 0x87, 0x86, 0x8c, 0xc1, 0x4f, 0x7e, 0x3b, 0x7a, 0xb7, + 0x60, 0xbd, 0xfe, 0x09, 0x36, 0x57, 0x84, 0x46, 0xd5, 0x8a, 0x77, 0xed, 0x1e, 0x11, + ]), + (167, [ + 0xdd, 0x6a, 0x1e, 0x62, 0x2b, 0x85, 0x93, 0x42, 0xbd, 0x87, 0x86, 0x8c, 0xc1, 0x4f, 0x7e, 0x3b, 0x7a, 0xb7, + 0x60, 0xbd, 0xfe, 0x09, 0x36, 0x57, 0x84, 0x46, 0xd5, 0x8a, 0x77, 0xed, 0x1e, 0x11, + ]), + (729, [ + 0xf0, 0x73, 0x89, 0xfc, 0x5a, 0xfb, 0x91, 0x51, 0x8c, 0xc0, 0x96, 0x00, 0x56, 0x29, 0x8a, 0xdf, 0x6d, 0x79, + 0x02, 0xb6, 0xda, 0x73, 0xe7, 0x28, 0x1c, 0x39, 0xbd, 0x97, 0xbd, 0xdb, 0x58, 0x0e, + ]), + (1290, [ + 0x18, 0x70, 0xa0, 0xef, 0x6a, 0xee, 0x4f, 0xbd, 0x8b, 0xdc, 0x9a, 0xb8, 0x8d, 0x71, 0xc3, 0x8d, 0x88, 0xf6, + 0xe3, 0x08, 0xc2, 0xdd, 0x53, 0x0b, 0x29, 0x7a, 0x75, 0x98, 0xe6, 0x58, 0xd2, 0x0d, + ]), + (1293, [ + 0x6d, 0x21, 0x12, 0x60, 0xee, 0xd8, 0xc3, 0xa6, 0x21, 0xae, 0x1d, 0xc0, 0xb2, 0x7d, 0xb9, 0x4d, 0x27, 0x38, + 0x6e, 0x35, 0x98, 0xed, 0x06, 0x90, 0x8d, 0x5d, 0xec, 0x0d, 0xe0, 0x4b, 0x83, 0x07, + ]), + (1295, [ + 0x6c, 0x0d, 0x33, 0xc0, 0x3f, 0x3d, 0x2e, 0x8e, 0xf3, 0x44, 0x66, 0xb3, 0x36, 0x99, 0x42, 0x18, 0xe9, 0x8f, + 0x5f, 0x74, 0xea, 0xfd, 0x32, 0xbf, 0x7a, 0x92, 0xee, 0x03, 0x8c, 0x21, 0x71, 0x0e, + ]), + (1296, [ + 0x6c, 0x0d, 0x33, 0xc0, 0x3f, 0x3d, 0x2e, 0x8e, 0xf3, 0x44, 0x66, 0xb3, 0x36, 0x99, 0x42, 0x18, 0xe9, 0x8f, + 0x5f, 0x74, 0xea, 0xfd, 0x32, 0xbf, 0x7a, 0x92, 0xee, 0x03, 0x8c, 0x21, 0x71, 0x0e, + ]), +]; + +// Deterministic input of a given length. +fn bits(len: usize) -> Vec { + let mut rng = TestRng::from_seed(1012147); + (0..len).map(|_| bool::rand(&mut rng)).collect() +} + +/// Input lengths to test. +fn representative_lengths(min_len: usize, max_len: usize) -> Vec { + let mid = (min_len + max_len) / 2; + let mut v = vec![ + min_len, + min_len + 1, + min_len + BHP_CHUNK_SIZE - 1, + min_len + BHP_CHUNK_SIZE, + min_len + BHP_CHUNK_SIZE + 1, + mid, + mid - (mid % BHP_CHUNK_SIZE), + max_len - 2 * BHP_CHUNK_SIZE, + max_len - BHP_CHUNK_SIZE, + max_len - 1, + max_len, + ]; + v.sort_unstable(); + v.dedup(); + v +} + +macro_rules! bhp_hasher_uncompressed_vector_test { + ($test_name:ident, $nw:literal, $ws:literal, $domain:literal, $expectations:ident) => { + #[test] + fn $test_name() -> Result<()> { + type H = BHPHasher; + let hasher = H::setup($domain)?; + let window_size = $ws; + let min_len = H::MIN_BITS + 1; + let max_len = H::MAX_BITS; + let lengths = representative_lengths(min_len, max_len); + let expected: &[(usize, [u8; 32])] = &$expectations[..]; + + assert_eq!( + lengths.len(), + expected.len(), + "{}: representative_lengths vs expectations row count", + stringify!($test_name) + ); + + let too_short = bits(window_size as usize * BHP_CHUNK_SIZE); + assert!(hasher.hash_uncompressed(&too_short).is_err()); + assert!(hasher.hash_uncompressed(&bits(max_len + 1)).is_err()); + + for (i, &n) in lengths.iter().enumerate() { + assert_eq!(n, expected[i].0, "{}: length mismatch at index {i}", stringify!($test_name)); + let input = bits(n); + assert_eq!(input.len(), n); + let digest = hasher.hash_uncompressed(&input)?; + let bytes = digest.to_bytes_le()?; + assert_eq!( + digest, + hasher.hash_uncompressed(&input)?, + "determinism check for {} at length {n}", + stringify!($test_name) + ); + assert_eq!( + bytes.as_slice(), + &expected[i].1[..], + "expectation mismatch for {} at input length {n}", + stringify!($test_name) + ); + } + + Ok(()) + } + }; +} + +bhp_hasher_uncompressed_vector_test!(test_bhp_vector_256, 3, 57, "BHPVec256", EXPECTATIONS_256); +bhp_hasher_uncompressed_vector_test!(test_bhp_vector_512, 6, 43, "BHPVec512", EXPECTATIONS_512); +bhp_hasher_uncompressed_vector_test!(test_bhp_vector_768, 15, 23, "BHPVec768", EXPECTATIONS_768); +bhp_hasher_uncompressed_vector_test!(test_bhp_vector_1024, 8, 54, "BHPVec1024", EXPECTATIONS_1024); + +#[test] +#[ignore] +fn print_bhp_expectations() -> Result<()> { + let print_row = |label: &str, n: usize, bytes: &[u8]| { + eprint!("EXPECTATIONS_{label} ({n}): ["); + for (i, b) in bytes.iter().enumerate() { + if i > 0 { + eprint!(", "); + } + eprint!("0x{b:02x}"); + } + eprintln!("]"); + }; + + macro_rules! display_expectations { + ($label:literal, $nw:literal, $ws:literal, $domain:literal) => {{ + type H = BHPHasher; + let h = H::setup($domain)?; + let min = H::MIN_BITS + 1; + let max = H::MAX_BITS; + eprintln!( + "--- BHP {} (WINDOW_SIZE = {} windows, NUM_WINDOWS = {}, domain = {}) ---", + $label, $ws, $nw, $domain + ); + for &n in &representative_lengths(min, max) { + print_row($label, n, &h.hash_uncompressed(&bits(n))?.to_bytes_le()?); + } + }}; + } + display_expectations!("256", 3, 57, "BHPVec256"); + display_expectations!("512", 6, 43, "BHPVec512"); + display_expectations!("768", 15, 23, "BHPVec768"); + display_expectations!("1024", 8, 54, "BHPVec1024"); + Ok(()) +} diff --git a/console/network/Cargo.toml b/console/network/Cargo.toml index accd18adf5..2a847fa70b 100644 --- a/console/network/Cargo.toml +++ b/console/network/Cargo.toml @@ -9,6 +9,7 @@ license = "Apache-2.0" edition = "2024" [features] +dev-print = [ "snarkvm-console-algorithms/dev-print", "snarkvm-console-network-environment/dev-print" ] filesystem = [ "snarkvm-parameters/filesystem" ] wasm = [ "snarkvm-parameters/wasm" diff --git a/console/network/src/consensus_heights.rs b/console/network/src/consensus_heights.rs index 0f246882fc..1fe9b9be00 100644 --- a/console/network/src/consensus_heights.rs +++ b/console/network/src/consensus_heights.rs @@ -16,6 +16,7 @@ use crate::{FromBytes, ToBytes, io_error}; use enum_iterator::{Sequence, last}; +use snarkvm_algorithms::snark::varuna::VarunaVersion; use std::io; /// The different consensus versions. @@ -57,6 +58,8 @@ pub enum ConsensusVersion { /// V15: Introduces the record-existence check and `commit.*.raw` instruction variants. /// Increase the anchor time to 35. V15 = 15, + /// V16: Switches to the use of SHA256 to digest circuit public parameters and inputs. + V16 = 16, } impl ToBytes for ConsensusVersion { @@ -84,6 +87,7 @@ impl FromBytes for ConsensusVersion { 13 => Ok(Self::V13), 14 => Ok(Self::V14), 15 => Ok(Self::V15), + 16 => Ok(Self::V16), _ => Err(io_error("Invalid consensus version")), } } @@ -121,7 +125,8 @@ pub const CANARY_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CON (ConsensusVersion::V12, 10_030_000), (ConsensusVersion::V13, 10_881_000), (ConsensusVersion::V14, 11_960_000), - (ConsensusVersion::V15, u32::MAX), + (ConsensusVersion::V15, u32::MAX - 1), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version height for `MainnetV0`. @@ -140,7 +145,8 @@ pub const MAINNET_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CO (ConsensusVersion::V12, 13_815_000), (ConsensusVersion::V13, 16_850_000), (ConsensusVersion::V14, 17_700_000), - (ConsensusVersion::V15, u32::MAX), + (ConsensusVersion::V15, u32::MAX - 1), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version heights for `TestnetV0`. @@ -159,7 +165,8 @@ pub const TESTNET_V0_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CO (ConsensusVersion::V12, 12_669_000), (ConsensusVersion::V13, 14_906_000), (ConsensusVersion::V14, 15_370_000), - (ConsensusVersion::V15, u32::MAX), + (ConsensusVersion::V15, u32::MAX - 1), + (ConsensusVersion::V16, u32::MAX), ]; /// The consensus version heights when the `test_consensus_heights` feature is enabled. @@ -179,6 +186,7 @@ pub const TEST_CONSENSUS_VERSION_HEIGHTS: [(ConsensusVersion, u32); NUM_CONSENSU (ConsensusVersion::V13, 16), (ConsensusVersion::V14, 17), (ConsensusVersion::V15, 18), + (ConsensusVersion::V16, 19), ]; #[cfg(any(test, feature = "test", feature = "test_consensus_heights"))] @@ -292,6 +300,18 @@ macro_rules! consensus_config_value_by_version { }; } +/// Returns the Varuna version for the specified consensus version. +pub fn varuna_version_from_consensus(consensus_version: ConsensusVersion) -> VarunaVersion { + // If new varuna versions are added, test_varuna_version_from_consensus below must be updated accordingly. + if consensus_version >= ConsensusVersion::V16 { + VarunaVersion::V3 + } else if consensus_version >= ConsensusVersion::V4 { + VarunaVersion::V2 + } else { + VarunaVersion::V1 + } +} + #[cfg(test)] mod tests { use super::*; @@ -553,4 +573,14 @@ mod tests { assert_eq!(TestnetV0::REWARD_ANCHOR_TIME, TestnetV0::ANCHOR_TIMES.first().unwrap().1); assert_eq!(CanaryV0::REWARD_ANCHOR_TIME, CanaryV0::ANCHOR_TIMES.first().unwrap().1); } + + #[test] + fn test_varuna_version_from_consensus() { + // First boundary: V4 + assert_eq!(varuna_version_from_consensus(ConsensusVersion::V3), VarunaVersion::V1); + assert_eq!(varuna_version_from_consensus(ConsensusVersion::V4), VarunaVersion::V2); + // Second boundary: V16 + assert_eq!(varuna_version_from_consensus(ConsensusVersion::V15), VarunaVersion::V2); + assert_eq!(varuna_version_from_consensus(ConsensusVersion::V16), VarunaVersion::V3); + } } diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 9ef59c579b..7a2d499b24 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -150,9 +150,9 @@ pub trait Network: /// The cost in microcredits per constraint for the deployment transaction. const SYNTHESIS_FEE_MULTIPLIER: u64 = 25; // 25 microcredits per constraint /// The maximum number of variables in a deployment. - const MAX_DEPLOYMENT_VARIABLES: u64 = 1 << 21; // 2,097,152 variables + const MAX_DEPLOYMENT_VARIABLES: u64 = 1 << 23; // 8,388,608 variables /// The maximum number of constraints in a deployment. - const MAX_DEPLOYMENT_CONSTRAINTS: u64 = 1 << 21; // 2,097,152 constraints + const MAX_DEPLOYMENT_CONSTRAINTS: u64 = 1 << 23; // 8,388,608 constraints /// The maximum number of instances to verify in a batch proof. const MAX_BATCH_PROOF_INSTANCES: usize = 128; /// The maximum number of microcredits that can be spent as a fee. diff --git a/console/program/Cargo.toml b/console/program/Cargo.toml index 4e43bfa981..77ac6da175 100644 --- a/console/program/Cargo.toml +++ b/console/program/Cargo.toml @@ -16,7 +16,11 @@ harness = false [features] default = [ ] test = [ ] -dev-print = [ "snarkvm-utilities/dev-print" ] +dev-print = [ + "snarkvm-utilities/dev-print", + "snarkvm-console-algorithms/dev-print", + "snarkvm-console-network/dev-print" +] [dependencies.snarkvm-console-account] workspace = true diff --git a/fields/src/traits/field.rs b/fields/src/traits/field.rs index bfada7bc19..4e566ca846 100644 --- a/fields/src/traits/field.rs +++ b/fields/src/traits/field.rs @@ -138,26 +138,22 @@ pub trait Field: /// least significant limb first. #[must_use] fn pow>(&self, exp: S) -> Self { - let mut res = Self::one(); + let mut bits = BitIteratorBE::new_without_leading_zeros(exp); - let mut found_one = false; + if bits.next().is_none() { + Self::one() + } else { + let mut res = *self; - for i in BitIteratorBE::new(exp) { - if !found_one { - if i { - found_one = true; - } else { - continue; - } - } + for bit in bits { + res.square_in_place(); - res.square_in_place(); - - if i { - res *= self; + if bit { + res *= self; + } } + res } - res } /// Returns a field element if the set of bytes forms a valid field element, diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index dfe415efc7..a355e607f2 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -44,6 +44,12 @@ path = "benches/advance.rs" harness = false required-features = [ "test-helpers", "rocks" ] +[[bench]] +name = "prepare_advance_multirecord" +path = "benches/prepare_advance_multirecord/main.rs" +harness = false +required-features = [ "test-helpers", "rocks" ] + [[bench]] name = "dag" path = "benches/dag.rs" diff --git a/ledger/benches/prepare_advance_multirecord/main.rs b/ledger/benches/prepare_advance_multirecord/main.rs new file mode 100644 index 0000000000..9237a14a4c --- /dev/null +++ b/ledger/benches/prepare_advance_multirecord/main.rs @@ -0,0 +1,380 @@ +// Copyright (c) 2019-2026 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Performs time measurements on prepare_advance_to_next_quorum_block for a block containing +a number of instances of the large one_to_many_records transaction. + - Generate artifacts with: + cargo bench --bench prepare_advance_multirecord -- --generate + - Artifacts are ignored by git. To clean them, run: + cargo bench --bench prepare_advance_multirecord -- --clean + - Obtain time measurements with: + cargo bench --bench prepare_advance_multirecord + The --serial feature can be added to deactivate parallelism. + - In order to run any of the above on a number of transfer_public instead, pass --transfer_public. +*/ + +use std::{env, fs, io::Write, path::Path, time::Instant}; + +use aleo_std::StorageMode; +use snarkvm_console::{ + account::{Address, PrivateKey, ViewKey}, + network::{ + MainnetV0, + prelude::{FromStr, Network, TestRng}, + }, + prelude::{FromBytes, ToBytes, Uniform}, + program::Value, +}; +use snarkvm_ledger::{ + Block, + Ledger, + Transaction, + store::{ConsensusStore, helpers::memory::ConsensusMemory}, + test_helpers::chain_builder::{GenerateBlockOptions, GenerateBlocksOptions, TestChainBuilder}, +}; +use snarkvm_synthesizer::{program::Program, vm::VM}; + +type CurrentNetwork = MainnetV0; + +// Consistent seeds so that transactions stored to disk can be checked later on +const CHAIN_ROOT_SEED: u64 = 25042026; +const GENESIS_COMMITTEE_SEED: u64 = 0x25042027; +const RNG_DEPLOY_ONE: u64 = 250420261; +const RNG_BLOCK_ONE: u64 = 250420262; +const RNG_DEPLOY_TWO: u64 = 250420263; +const RNG_BLOCK_TWO: u64 = 250420264; +const RNG_EXECUTE: u64 = 250420265; +const RNG_PREPARE_BLOCK: u64 = 250420266; +const DETERMINISTIC_WARMUP_TIMESTAMP: i64 = CurrentNetwork::GENESIS_TIMESTAMP + 1; + +/// Mirrors `TestChainBuilder::initialize_components` with a fixed committee RNG seed so the beacon +/// operator key is available for deploy/execute (that API is not exposed on `TestChainBuilder`). +fn genesis_with_fixed_committee_seed( + rng: &mut TestRng, + committee_size: usize, + genesis_committee_seed: u64, +) -> (PrivateKey, Vec>, Block) { + let genesis_pk = PrivateKey::new(rng).unwrap(); + let store = ConsensusStore::<_, ConsensusMemory<_>>::open(StorageMode::new_test(None)).unwrap(); + let genesis_rng = &mut TestRng::from_seed(genesis_committee_seed); + let genesis = VM::from(store).unwrap().genesis_beacon(&genesis_pk, genesis_rng).unwrap(); + let genesis_rng = &mut TestRng::from_seed(genesis_committee_seed); + let committee_keys = (0..committee_size).map(|_| PrivateKey::new(genesis_rng).unwrap()).collect(); + (genesis_pk, committee_keys, genesis) +} + +fn main() { + /////////////////////////// User defined + // Number of transactions to generate and check in each of the two cases: + // simple (transfer_public) and complex (one_to_many_records). Right now, + // this is set to ConsensusState::MAXIMUM_CONFIRMED_TRANSACTIONS, which is 8 + // in test environments. + let n_transactions = 8; + /////////////////////////// + + let generate = env::args().any(|arg| arg == "--generate"); + let clean = env::args().any(|arg| arg == "--clean"); + let artifact_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("benches/prepare_advance_multirecord/artifacts"); + + if clean { + if generate { + panic!( + "--clean and --generate cannot be used together. Use --generate to generate\\ + the artifacts, --clean to delete them (and end), and neither to use the existing artifacts." + ); + } + std::fs::remove_dir_all(&artifact_path).unwrap(); + println!("Artifacts deleted."); + return; + } + + if !artifact_path.exists() { + if !generate { + panic!("--generate was not passed, but artifacts were not found."); + } + std::fs::create_dir_all(&artifact_path).unwrap(); + } + + let rng = &mut TestRng::from_seed(CHAIN_ROOT_SEED); + + let (genesis_pk, committee_keys, genesis) = genesis_with_fixed_committee_seed(rng, 4, GENESIS_COMMITTEE_SEED); + + let mut chain_builder = TestChainBuilder::from_components(committee_keys, genesis.clone()).unwrap(); + let ledger = + Ledger::>::load(genesis, StorageMode::new_test(None)).unwrap(); + let num_validators = chain_builder.private_keys().len(); + + for block in chain_builder + .generate_blocks_with_opts( + 64, + GenerateBlocksOptions { + skip_to_current_version: true, + num_validators, + deterministic_block_timestamp_start: Some(DETERMINISTIC_WARMUP_TIMESTAMP), + ..Default::default() + }, + rng, + ) + .unwrap() + { + ledger.advance_to_next_block(&block).unwrap(); + } + + let program = Program::::from_str( + r" + program one_to_many_records.aleo; + constructor: + assert.eq true true; + record test: + owner as address.private; + amount as u64.private; + function mint: + input r0 as u64.private; + cast self.caller r0 into r1 as test.record; + output r1 as test.record; + function one_to_many_records: + input r0 as u64.private; + input r1 as address.private; + input r2 as u64.private; + cast r1 r2 into r3 as test.record; + cast r1 r2 into r4 as test.record; + cast r1 r2 into r5 as test.record; + cast r1 r2 into r6 as test.record; + cast r1 r2 into r7 as test.record; + cast r1 r2 into r8 as test.record; + cast r1 r2 into r9 as test.record; + cast r1 r2 into r10 as test.record; + cast r1 r2 into r11 as test.record; + cast r1 r2 into r12 as test.record; + cast r1 r2 into r13 as test.record; + cast r1 r2 into r14 as test.record; + cast r1 r2 into r15 as test.record; + cast r1 r2 into r16 as test.record; + cast r1 r2 into r17 as test.record; + cast r1 r2 into r18 as test.record; + output r3 as test.record; + output r4 as test.record; + output r5 as test.record; + output r6 as test.record; + output r7 as test.record; + output r8 as test.record; + output r9 as test.record; + output r10 as test.record; + output r11 as test.record; + output r12 as test.record; + output r13 as test.record; + output r14 as test.record; + output r15 as test.record; + output r16 as test.record; + output r17 as test.record; + output r18 as test.record; + ", + ) + .unwrap(); + + let mut wrapper_program = r" + import one_to_many_records.aleo; + program wrapper.aleo; + constructor: + assert.eq true true; + function call_one_to_many_records: + input r0 as u64.private; + input r1 as address.private; + input r2 as u64.private;" + .to_string(); + + let call = |start_index: usize| { + let mut call_str = " call one_to_many_records.aleo/one_to_many_records r0 r1 r2 into".to_string(); + for i in start_index..start_index + 16 { + call_str.push_str(&format!(" r{i}")); + } + call_str.push_str(";\n"); + call_str + }; + for i in 0..30 { + let start_index = 3 + (i * 16); + wrapper_program.push_str(&call(start_index)); + } + let wrapper_program = Program::from_str(&wrapper_program).unwrap(); + + let case = + if env::args().any(|arg| arg == "--transfer_public") { "transfer_public" } else { "one_to_many_records" }; + + if case == "one_to_many_records" { + let deploy_one_rng = &mut TestRng::from_seed(RNG_DEPLOY_ONE); + + println!("Deploying one_to_many_records.aleo"); + let deployment_one = ledger.vm().deploy(&genesis_pk, &program, None, 0, None, deploy_one_rng).unwrap(); + + let block_one_rng = &mut TestRng::from_seed(RNG_BLOCK_ONE); + + let block_one = chain_builder + .generate_block_with_opts( + GenerateBlockOptions { + transactions: vec![deployment_one], + timestamp: chain_builder + .ledger() + .latest_timestamp() + .saturating_add(CurrentNetwork::BLOCK_TIME as i64), + ..Default::default() + }, + block_one_rng, + ) + .unwrap(); + ledger.advance_to_next_block(&block_one).unwrap(); + + assert_eq!(block_one.transactions().num_accepted(), 1); + + let deploy_two_rng = &mut TestRng::from_seed(RNG_DEPLOY_TWO); + + println!("Deploying wrapper.aleo"); + let deployment_two = ledger.vm().deploy(&genesis_pk, &wrapper_program, None, 0, None, deploy_two_rng).unwrap(); + + let block_two_rng = &mut TestRng::from_seed(RNG_BLOCK_TWO); + + let block_two = chain_builder + .generate_block_with_opts( + GenerateBlockOptions { + transactions: vec![deployment_two], + timestamp: chain_builder + .ledger() + .latest_timestamp() + .saturating_add(CurrentNetwork::BLOCK_TIME as i64), + ..Default::default() + }, + block_two_rng, + ) + .unwrap(); + ledger.advance_to_next_block(&block_two).unwrap(); + + assert_eq!(block_two.transactions().num_accepted(), 1); + } + + let genesis_view_key = ViewKey::try_from(&genesis_pk).unwrap(); + let genesis_address = Address::try_from(&genesis_view_key).unwrap(); + let recipient_address: Address = Address::rand(&mut TestRng::from_seed(112233)); + + let full_artifact_path = artifact_path.join(case); + + let multi_record_transactions = if generate { + println!("Generating artifacts for {n_transactions} {case} transaction(s)"); + + fs::create_dir_all(&full_artifact_path).unwrap(); + + (0..n_transactions) + .map(|i| { + print!(" Executing {case} transaction {i}"); + std::io::stdout().flush().unwrap(); + + let timer = Instant::now(); + let execute_rng = &mut TestRng::from_seed(RNG_EXECUTE.wrapping_add(i as u64)); + + let tx = if case == "one_to_many_records" { + ledger + .vm() + .execute( + &genesis_pk, + ("wrapper.aleo", "call_one_to_many_records"), + [ + Value::from_str(&format!("{i}u64")).unwrap(), + Value::from_str(&format!("{genesis_address}")).unwrap(), + Value::from_str(&format!("{}u64", 10_000 + i)).unwrap(), + ] + .into_iter(), + None, + 0, + None, + execute_rng, + ) + .unwrap() + } else { + ledger + .vm() + .execute( + &genesis_pk, + ("credits.aleo", "transfer_public"), + [ + Value::from_str(&format!("{recipient_address}")).unwrap(), + Value::from_str(&format!("{}u64", 10_000 + i)).unwrap(), + ] + .into_iter(), + None, + 0, + None, + execute_rng, + ) + .unwrap() + }; + + println!(" (finished in {:.2}s)", timer.elapsed().as_secs_f32()); + + let check_rng = &mut TestRng::from_seed(RNG_EXECUTE.wrapping_add(1000 + i as u64)); + assert!(ledger.vm().check_transaction(&tx, None, check_rng).is_ok()); + + fs::write(full_artifact_path.join(format!("transaction_{i}.bin")), tx.to_bytes_le().unwrap()) + .unwrap_or_else(|_| panic!("Failed to write artifact for transaction {i}")); + + tx + }) + .collect::>() + } else { + println!("Loading artifacts for {n_transactions} {case} transaction(s)"); + + (0..n_transactions) + .map(|i| { + Transaction::from_bytes_le( + &std::fs::read(full_artifact_path.join(format!("transaction_{i}.bin"))) + .unwrap_or_else(|_| panic!("Failed to load transaction {i}")), + ) + .unwrap() + }) + .collect::>() + }; + + let prepare_rng = &mut TestRng::from_seed(RNG_PREPARE_BLOCK); + + let (subdag, transmissions, leader_certificate) = chain_builder + .build_quorum_subdag_and_transmissions_for_next_block( + GenerateBlockOptions { + transactions: multi_record_transactions, + timestamp: chain_builder.ledger().latest_timestamp().saturating_add(CurrentNetwork::BLOCK_TIME as i64), + ..Default::default() + }, + prepare_rng, + ) + .unwrap(); + + println!("Proceeding to next block..."); + + let timer = Instant::now(); + let block = + chain_builder.ledger().prepare_advance_to_next_quorum_block(subdag, transmissions, prepare_rng).unwrap(); + let prepare_ms = timer.elapsed().as_millis(); + + println!(" * prepare_advance_to_next_quorum_block finished in {prepare_ms}ms"); + + let timer = Instant::now(); + chain_builder.apply_prepared_quorum_block(&block, leader_certificate).unwrap(); + let apply_ms = timer.elapsed().as_millis(); + println!(" * apply_prepared_quorum_block finished in {apply_ms}ms"); + + let timer = Instant::now(); + ledger.advance_to_next_block(&block).unwrap(); + let advance_ms = timer.elapsed().as_millis(); + println!(" * advance_to_next_block finished in {advance_ms}ms"); + + assert_eq!(block.transactions().num_accepted(), n_transactions); +} diff --git a/ledger/src/test_helpers/chain_builder.rs b/ledger/src/test_helpers/chain_builder.rs index 52417aa14f..f7992ac3bc 100644 --- a/ledger/src/test_helpers/chain_builder.rs +++ b/ledger/src/test_helpers/chain_builder.rs @@ -81,6 +81,9 @@ pub struct GenerateBlocksOptions { pub num_validators: usize, /// Preloaded transactions to populate the blocks with. pub transactions: Vec>, + /// When set, [`TestChainBuilder::generate_blocks_with_opts`] assigns quorum block timestamps as + /// `start`, `start + 1`, … instead of wall-clock time so chains match across processes. + pub deterministic_block_timestamp_start: Option, } impl Default for GenerateBlocksOptions { @@ -91,6 +94,7 @@ impl Default for GenerateBlocksOptions { skip_to_current_version: false, num_validators: 0, transactions: Default::default(), + deterministic_block_timestamp_start: None, } } } @@ -226,6 +230,17 @@ impl TestChainBuilder { assert!(num_blocks > 0, "Need to build at least one block"); let mut result = vec![]; + let mut deterministic_ts = options.deterministic_block_timestamp_start; + + let mut next_block_timestamp = || -> i64 { + match deterministic_ts { + Some(t) => { + deterministic_ts = Some(t.saturating_add(1)); + t + } + None => OffsetDateTime::now_utc().unix_timestamp(), + } + }; // If configured, skip enough blocks to reach the current consensus version. if options.skip_to_current_version { @@ -241,6 +256,7 @@ impl TestChainBuilder { let options = GenerateBlockOptions { skip_votes: options.skip_votes, skip_nodes: options.skip_nodes.clone(), + timestamp: next_block_timestamp(), ..Default::default() }; @@ -263,6 +279,7 @@ impl TestChainBuilder { skip_votes: options.skip_votes, skip_nodes: options.skip_nodes.clone(), transactions: options.transactions.drain(..num_txs).collect(), + timestamp: next_block_timestamp(), ..Default::default() }; @@ -280,12 +297,23 @@ impl TestChainBuilder { self.generate_block_with_opts(GenerateBlockOptions::default(), rng) } - /// Same as `generate_block` but with additional options/parameters. - pub fn generate_block_with_opts( + /// Returns a read-only handle to the ledger under construction. + pub fn ledger(&self) -> &Ledger> { + &self.ledger + } + + /// Builds the Narwhal subdag and transmission map for the next quorum block, updating internal + /// Narwhal bookkeeping as if the block were committed, but **without** calling + /// [`Ledger::prepare_advance_to_next_quorum_block`] or advancing the ledger. + /// + /// Pair with [`Ledger::prepare_advance_to_next_quorum_block`] and + /// [`Self::apply_prepared_quorum_block`] to benchmark preparation separately from advancement. + #[allow(clippy::type_complexity)] + pub fn build_quorum_subdag_and_transmissions_for_next_block( &mut self, options: GenerateBlockOptions, rng: &mut TestRng, - ) -> Result> { + ) -> Result<(Subdag, IndexMap, Transmission>, BatchCertificate)> { assert!( options.skip_nodes.len() * 3 < self.private_keys.len(), "Cannot mark more than f nodes as unavailable/skipped" @@ -463,6 +491,33 @@ impl TestChainBuilder { // Construct the block. let subdag = Subdag::from(subdag_map).unwrap(); + Ok((subdag, transmissions, leader_certificate)) + } + + /// Advances the builder ledger after [`Ledger::prepare_advance_to_next_quorum_block`], and + /// updates Narwhal leader bookkeeping. Call with the same `leader_certificate` returned from + /// [`Self::build_quorum_subdag_and_transmissions_for_next_block`] for this block. + pub fn apply_prepared_quorum_block( + &mut self, + block: &Block, + leader_certificate: BatchCertificate, + ) -> Result<()> { + self.ledger + .advance_to_next_block(block) + .with_context(|| "Failed to (internally) advance to generated block")?; + self.previous_leader_certificate = Some(leader_certificate); + Ok(()) + } + + /// Same as `generate_block` but with additional options/parameters. + pub fn generate_block_with_opts( + &mut self, + options: GenerateBlockOptions, + rng: &mut TestRng, + ) -> Result> { + let (subdag, transmissions, leader_certificate) = + self.build_quorum_subdag_and_transmissions_for_next_block(options, rng)?; + let block = self.ledger.prepare_advance_to_next_quorum_block(subdag, transmissions, rng)?; // Skip to increase performance. @@ -470,11 +525,7 @@ impl TestChainBuilder { trace!("Generated new block {} at height {}", block.hash(), block.height()); - // Update the ledger state. - self.ledger - .advance_to_next_block(&block) - .with_context(|| "Failed to (internally) advance to generated block")?; - self.previous_leader_certificate = Some(leader_certificate.clone()); + self.apply_prepared_quorum_block(&block, leader_certificate)?; trace!("Updated internal ledger to height {}", block.height()); Ok(block) diff --git a/synthesizer/Cargo.toml b/synthesizer/Cargo.toml index 648cbd45a8..540c295111 100644 --- a/synthesizer/Cargo.toml +++ b/synthesizer/Cargo.toml @@ -60,7 +60,7 @@ wasm = [ "snarkvm-synthesizer-program/wasm", "snarkvm-synthesizer-snark/wasm" ] -dev-print = [ "snarkvm-utilities/dev-print" ] +dev-print = [ "snarkvm-utilities/dev-print", "snarkvm-console/dev-print" ] dev_skip_checks = [ "snarkvm-synthesizer-process/dev_skip_checks" ] snark-print = [ "snarkvm-algorithms/snark-print" ] test_consensus_heights = [ "snarkvm-synthesizer-process/test_consensus_heights" ] @@ -71,6 +71,11 @@ name = "kary_merkle_tree" path = "benches/kary_merkle_tree.rs" harness = false +[[bench]] +name = "check_transaction_multirecord" +path = "benches/check_transaction_multirecord/main.rs" +harness = false + [dependencies.snarkvm-algorithms] workspace = true diff --git a/synthesizer/benches/check_transaction_multirecord/main.rs b/synthesizer/benches/check_transaction_multirecord/main.rs new file mode 100644 index 0000000000..ad0af66f3e --- /dev/null +++ b/synthesizer/benches/check_transaction_multirecord/main.rs @@ -0,0 +1,349 @@ +// Copyright (c) 2019-2026 Provable Inc. +// This file is part of the snarkVM library. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Performs time measurements on the verification of the large one_to_many_records transaction. + - Generate artifacts (both the blocks where programs are deployed and the transactions themselves) with: + cargo bench --bench check_transaction_multirecord -- --generate + - Artifacts are ignored by git. To clean them, run: + cargo bench --bench check_transaction_multirecord -- --clean + - Obtain time measurements with: + cargo bench --bench check_transaction_multirecord + The --serial feature can be added to deactivate parallelism. + - Flamegraph with: + cargo flamegraph --bench check_transaction_multirecord --features serial +*/ + +use std::{env, path::Path, time::Instant}; + +use snarkvm_console::{ + account::{Address, PrivateKey, ViewKey}, + network::{ + MainnetV0, + prelude::{ConsensusVersion, CryptoRng, FromStr, Network, Result, Rng, TestRng, Zero}, + }, + program::Value, + types::Field, +}; +use snarkvm_ledger_block::{Block, Header, Metadata, Transaction}; +use snarkvm_ledger_store::{ConsensusStore, helpers::memory::ConsensusMemory}; +use snarkvm_synthesizer::VM; +use snarkvm_synthesizer_program::{FinalizeGlobalState, Program}; + +use aleo_std::StorageMode; +use snarkvm_utilities::{FromBytes, ToBytes}; + +type CurrentNetwork = MainnetV0; +type CurrentLedger = ConsensusMemory; + +fn sample_next_block( + vm: &VM, + private_key: &PrivateKey, + transactions: &[Transaction], + rng: &mut R, +) -> Result> { + let block_hash = vm.block_store().get_block_hash(vm.block_store().max_height().unwrap()).unwrap().unwrap(); + let previous_block = vm.block_store().get_block(&block_hash).unwrap().unwrap(); + + let next_block_height = previous_block.height() + 1; + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; + let next_block_timestamp = previous_block.timestamp().saturating_add(time_since_last_block); + let next_timestamp = (next_block_height + >= CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V12).unwrap_or_default()) + .then_some(next_block_timestamp); + let finalize_state = + FinalizeGlobalState::from(next_block_height as u64, next_block_height, next_timestamp, [0u8; 32]); + + let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = + vm.speculate(finalize_state, time_since_last_block, None, vec![], &None.into(), transactions.iter(), rng)?; + + let metadata = Metadata::new( + CurrentNetwork::ID, + previous_block.round() + 1, + previous_block.height() + 1, + 0, + 0, + CurrentNetwork::GENESIS_COINBASE_TARGET, + CurrentNetwork::GENESIS_PROOF_TARGET, + previous_block.last_coinbase_target(), + previous_block.last_coinbase_timestamp(), + previous_block.timestamp().saturating_add(time_since_last_block), + )?; + + let header = Header::from( + vm.block_store().current_state_root(), + transactions.to_transactions_root().unwrap(), + transactions.to_finalize_root(ratified_finalize_operations).unwrap(), + ratifications.to_ratifications_root().unwrap(), + Field::zero(), + Field::zero(), + metadata, + )?; + + Block::new_beacon( + private_key, + previous_block.hash(), + header, + ratifications, + None.into(), + vec![], + transactions, + aborted_transaction_ids, + rng, + ) +} + +fn main() { + /////////////////////////// User defined + // Number of times to verify the transaction (when not in --generate mode). + // A higher number helps flamegraph get more precise measurements. + let n_samples = 10; + /////////////////////////// + + let generate = env::args().any(|arg| arg == "--generate"); + let clean = env::args().any(|arg| arg == "--clean"); + let artifact_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("benches/check_transaction_multirecord/artifacts"); + let transaction_path = artifact_path.join("transactions"); + + if clean { + if generate { + panic!( + "--clean and --generate cannot be used together. Use --generate to generate\\ + the artifacts, --clean to delete them (and end), and neither to use the existing artifacts." + ); + } + std::fs::remove_dir_all(&artifact_path).unwrap(); + println!("Artifacts deleted."); + return; + } + + if !transaction_path.exists() { + if !generate { + panic!("--generate was not passed, but artifacts were not found."); + } + std::fs::create_dir_all(&transaction_path).unwrap(); + } + + let rng = &mut TestRng::from_seed(160426); + + // Generate the genesis private key. + let private_key = PrivateKey::::new(rng).unwrap(); + + // Generate the genesis block using a temporary VM. + let genesis = { + let vm = VM::::from(ConsensusStore::open(StorageMode::new_test(None)).unwrap()) + .unwrap(); + vm.genesis_beacon(&private_key, rng).unwrap() + }; + + // Initialize the VM. + let vm = + VM::::from(ConsensusStore::open(StorageMode::new_test(None)).unwrap()).unwrap(); + + // Add the genesis block. + vm.add_next_block(&genesis).unwrap(); + + // Advance the ledger to ConsensusV16 + let transactions: [Transaction; 0] = []; + while vm.block_store().current_block_height() < CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V16).unwrap() { + let next_block = sample_next_block(&vm, &private_key, &transactions, rng).unwrap(); + vm.add_next_block(&next_block).unwrap(); + } + + let view_key = ViewKey::try_from(&private_key).unwrap(); + let address = Address::try_from(&view_key).unwrap(); + + // Deploy a test program to the ledger. + let program = Program::::from_str( + r" + program one_to_many_records.aleo; + constructor: + assert.eq true true; + record test: + owner as address.private; + amount as u64.private; + function mint: + input r0 as u64.private; + cast self.caller r0 into r1 as test.record; + output r1 as test.record; + function one_to_many_records: + input r0 as u64.private; // dummy input + input r1 as address.private; + input r2 as u64.private; + cast r1 r2 into r3 as test.record; + cast r1 r2 into r4 as test.record; + cast r1 r2 into r5 as test.record; + cast r1 r2 into r6 as test.record; + cast r1 r2 into r7 as test.record; + cast r1 r2 into r8 as test.record; + cast r1 r2 into r9 as test.record; + cast r1 r2 into r10 as test.record; + cast r1 r2 into r11 as test.record; + cast r1 r2 into r12 as test.record; + cast r1 r2 into r13 as test.record; + cast r1 r2 into r14 as test.record; + cast r1 r2 into r15 as test.record; + cast r1 r2 into r16 as test.record; + cast r1 r2 into r17 as test.record; + cast r1 r2 into r18 as test.record; + output r3 as test.record; + output r4 as test.record; + output r5 as test.record; + output r6 as test.record; + output r7 as test.record; + output r8 as test.record; + output r9 as test.record; + output r10 as test.record; + output r11 as test.record; + output r12 as test.record; + output r13 as test.record; + output r14 as test.record; + output r15 as test.record; + output r16 as test.record; + output r17 as test.record; + output r18 as test.record; + ", + ) + .unwrap(); + + // Wrapper program to call the one_to_many_records function. + let mut wrapper_program = r" + import one_to_many_records.aleo; + program wrapper.aleo; + constructor: + assert.eq true true; + function call_one_to_many_records: + input r0 as u64.private; + input r1 as address.private; + input r2 as u64.private;" + .to_string(); + + // Append calls to the one_to_many_records function. + let call = |start_index: usize| { + let mut call_str = " call one_to_many_records.aleo/one_to_many_records r0 r1 r2 into".to_string(); + for i in start_index..start_index + 16 { + call_str.push_str(&format!(" r{i}")); + } + call_str.push_str(";\n"); + call_str + }; + for i in 0..30 { + let start_index = 3 + (i * 16); + wrapper_program.push_str(&call(start_index)); + } + let wrapper_program = Program::from_str(&wrapper_program).unwrap(); + + // Deploy the first program + let deployment_block_path = artifact_path.join("deployment_block_1.bin"); + if generate { + println!("Deploying program one_to_many_records.aleo (generating block)"); + let deployment = vm.deploy(&private_key, &program, None, 0, None, rng).unwrap(); + + let deployment_block = sample_next_block(&vm, &private_key, &[deployment.clone()], rng).unwrap(); + assert_eq!(deployment_block.transactions().num_accepted(), 1); + vm.add_next_block(&deployment_block).unwrap(); + + std::fs::write( + deployment_block_path, + deployment_block.to_bytes_le().expect("Failed to write deployment block for first program"), + ) + .unwrap(); + } else { + println!("Deploying program one_to_many_records.aleo (loading block)"); + let deployment_block = Block::from_bytes_le( + &std::fs::read(deployment_block_path).expect("Deployment block for first program not found"), + ) + .unwrap(); + vm.add_next_block(&deployment_block).unwrap(); + } + + // Deploy the second program + let deployment_block_path = artifact_path.join("deployment_block_2.bin"); + if generate { + println!("Deploying program wrapper.aleo (generating block)"); + let deployment_wrapper = vm.deploy(&private_key, &wrapper_program, None, 0, None, rng).unwrap(); + let deployment_block = sample_next_block(&vm, &private_key, &[deployment_wrapper], rng).unwrap(); + assert_eq!(deployment_block.transactions().num_accepted(), 1); + vm.add_next_block(&deployment_block).unwrap(); + + std::fs::write( + deployment_block_path, + deployment_block.to_bytes_le().expect("Failed to write deployment block for second program"), + ) + .unwrap(); + } else { + println!("Deploying program wrapper.aleo (loading block)"); + let deployment_block = Block::from_bytes_le( + &std::fs::read(deployment_block_path).expect("Deployment block for second program not found"), + ) + .unwrap(); + vm.add_next_block(&deployment_block).unwrap(); + } + + // Load or execute wrapper call + if generate { + println!("Executing wrapper call (generating transaction) {n_samples} times..."); + + for i in 0..n_samples { + let current_transaction_path = transaction_path.join(format!("transaction_{i}.bin")); + + let value1_str = format!("{i}u64"); + let value2_str = format!("{}u64", 10_000 + i); + + let transaction = vm + .execute( + &private_key, + ("wrapper.aleo", "call_one_to_many_records"), + vec![ + Value::from_str(&value1_str).unwrap(), + Value::from_str(&format!("{address}")).unwrap(), + Value::from_str(&value2_str).unwrap(), + ] + .into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + + assert!(vm.check_transaction(&transaction, None, rng).is_ok()); + + std::fs::write(¤t_transaction_path, transaction.to_bytes_le().unwrap()).unwrap(); + } + } else { + println!("Loading {n_samples} transactions..."); + + let transactions = (0..n_samples) + .map(|i| { + Transaction::from_bytes_le( + &std::fs::read(transaction_path.join(format!("transaction_{i}.bin"))) + .unwrap_or_else(|_| panic!("Transaction {i} not found")), + ) + .unwrap() + }) + .collect::>(); + + println!("Checking {n_samples} transactions..."); + + let timer = Instant::now(); + for tx in transactions.iter().take(n_samples) { + assert!(vm.check_transaction(tx, None, rng).is_ok()); + } + let elapsed = timer.elapsed().as_micros() as f64 / 1000.0; + let elapsed_avg = elapsed / n_samples as f64; + println!("Transactions checked in {elapsed:.2} ms ({elapsed_avg:.2} ms per transaction)"); + } +} diff --git a/synthesizer/process/src/verify_execution/mod.rs b/synthesizer/process/src/verify_execution/mod.rs index 7532b24cfa..a7b038faab 100644 --- a/synthesizer/process/src/verify_execution/mod.rs +++ b/synthesizer/process/src/verify_execution/mod.rs @@ -15,6 +15,9 @@ use super::*; +#[cfg(not(feature = "serial"))] +use rayon::prelude::*; + mod ensure_records_exist; impl Process { @@ -137,7 +140,8 @@ impl Process { // Ensure each output is valid. let num_inputs = transition.inputs().len(); let num_outputs = transition.outputs().len(); - for (index, output) in transition.outputs().iter().enumerate() { + let tcm = transition.tcm(); + cfg_iter!(transition.outputs()).enumerate().try_for_each(|(index, output)| { // If the consensus version are before `ConsensusVersion::V8`, ensure the output record is on Version 0. // if the consensus version is on or after `ConsensusVersion::V8`, ensure the output record is on Version 1. if let Some((_, record)) = output.record() { @@ -154,10 +158,11 @@ impl Process { } } // Ensure the output is valid. - if !output.verify(function_id, transition.tcm(), num_inputs + index) { + if !output.verify(function_id, tcm, num_inputs + index) { bail!("Failed to verify a transition output") } - } + Ok(()) + })?; lap!(timer, "Verify the outputs"); // Retrieve the stack. diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index 2ff25284e9..9758e859c6 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -17,6 +17,7 @@ use super::*; +use console::network::varuna_version_from_consensus; use snarkvm_synthesizer_error::*; impl> VM { @@ -195,10 +196,7 @@ impl> VM { // Check whether the authorization is creating valid records. authorization.check_valid_records(consensus_version)?; // Determine which Varuna version to use. - let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) { - true => VarunaVersion::V1, - false => VarunaVersion::V2, - }; + let varuna_version = varuna_version_from_consensus(consensus_version); macro_rules! logic { ($process:expr, $network:path, $aleo:path) => {{ // Prepare the authorization. @@ -253,10 +251,7 @@ impl> VM { // Check whether the authorization is creating valid records. authorization.check_valid_records(consensus_version)?; // Determine which Varuna version to use. - let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) { - true => VarunaVersion::V1, - false => VarunaVersion::V2, - }; + let varuna_version = varuna_version_from_consensus(consensus_version); macro_rules! logic { ($process:expr, $network:path, $aleo:path) => {{ // Prepare the authorization. diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index 1e8324b85c..c8e174d570 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -44,7 +44,6 @@ use console::{ }, types::{Field, Group, U16, U64}, }; -use snarkvm_algorithms::snark::varuna::VarunaVersion; use snarkvm_ledger_block::{ Block, ConfirmedTransaction, diff --git a/synthesizer/src/vm/tests/test_v14/snark_verify.rs b/synthesizer/src/vm/tests/test_v14/snark_verify.rs index 47602a354e..ccee3a630e 100644 --- a/synthesizer/src/vm/tests/test_v14/snark_verify.rs +++ b/synthesizer/src/vm/tests/test_v14/snark_verify.rs @@ -17,6 +17,7 @@ use super::*; use circuit::{Circuit, Environment}; use console::algorithms::U8; +use snarkvm_algorithms::snark::varuna::VarunaVersion; use snarkvm_synthesizer_snark::{ProvingKey, UniversalSRS}; use std::sync::OnceLock; diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 11be654446..a0ffcdb3fb 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use console::network::varuna_version_from_consensus; + use super::*; /// Ensures the given iterator has no duplicate elements, and that the ledger @@ -800,10 +802,7 @@ impl> VM { // Determine which consensus version to use. let consensus_version = N::CONSENSUS_VERSION(block_height)?; // Determine which Varuna version to use. - let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) { - true => VarunaVersion::V1, - false => VarunaVersion::V2, - }; + let varuna_version = varuna_version_from_consensus(consensus_version); // Determine the inclusion version to use. let is_network_behind_upgrade_height = block_height < N::INCLUSION_UPGRADE_HEIGHT()?; let inclusion_version = match (ConsensusVersion::V1..=ConsensusVersion::V7).contains(&consensus_version) @@ -871,10 +870,7 @@ impl> VM { // Determine which Varuna version to use. let consensus_version = N::CONSENSUS_VERSION(block_height)?; - let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) { - true => VarunaVersion::V1, - false => VarunaVersion::V2, - }; + let varuna_version = varuna_version_from_consensus(consensus_version); // Determine the inclusion version to use. let is_network_behind_upgrade_height = block_height < N::INCLUSION_UPGRADE_HEIGHT()?; let inclusion_version = match (ConsensusVersion::V1..=ConsensusVersion::V7).contains(&consensus_version) diff --git a/vm/package/execute.rs b/vm/package/execute.rs index 483e7bf771..fe72750d36 100644 --- a/vm/package/execute.rs +++ b/vm/package/execute.rs @@ -16,6 +16,7 @@ use super::*; use anyhow::Context; +use snarkvm_console::network::varuna_version_from_consensus; impl Package { /// Executes a program function with the given inputs. @@ -117,10 +118,8 @@ impl Package { let call_metrics = trace.call_metrics().to_vec(); // Determine which Varuna version to use. - let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) { - true => VarunaVersion::V1, - false => VarunaVersion::V2, - }; + let varuna_version = varuna_version_from_consensus(consensus_version); + // Prepare the trace. trace.prepare(&query)?; diff --git a/vm/package/mod.rs b/vm/package/mod.rs index 1e10d7362c..5089692cc5 100644 --- a/vm/package/mod.rs +++ b/vm/package/mod.rs @@ -23,7 +23,6 @@ mod run; pub use deploy::{DeployRequest, DeployResponse}; use crate::{ - algorithms::snark::varuna::VarunaVersion, console::{ account::PrivateKey, network::{ConsensusVersion, Network},