diff --git a/examples/and.rs b/examples/and.rs new file mode 100644 index 000000000..147f07d30 --- /dev/null +++ b/examples/and.rs @@ -0,0 +1,338 @@ +//! This example executes a batch of 64-bit AND operations. +//! It performs the AND operation by first decomposing the operands into bits and then performing the operation bit-by-bit. +//! We execute a configurable number of AND operations per step of Nova's recursion. +use arecibo::{ + provider::{Bn256EngineKZG, GrumpkinEngine}, + traits::{ + circuit::{StepCircuit, TrivialCircuit}, + snark::RelaxedR1CSSNARKTrait, + Engine, Group, + }, + CompressedSNARK, PublicParams, RecursiveSNARK, +}; +use bellpepper_core::{ + boolean::AllocatedBit, num::AllocatedNum, ConstraintSystem, LinearCombination, SynthesisError, +}; +use core::marker::PhantomData; +use ff::Field; +use ff::{PrimeField, PrimeFieldBits}; +use flate2::{write::ZlibEncoder, Compression}; +use halo2curves::bn256::Bn256; +use rand::Rng; +use std::time::Instant; + +type E1 = Bn256EngineKZG; +type E2 = GrumpkinEngine; +type EE1 = arecibo::provider::mlkzg::EvaluationEngine; +type EE2 = arecibo::provider::ipa_pc::EvaluationEngine; +type S1 = arecibo::spartan::snark::RelaxedR1CSSNARK; // non-preprocessing SNARK +type S2 = arecibo::spartan::snark::RelaxedR1CSSNARK; // non-preprocessing SNARK + +#[derive(Clone, Debug)] +struct AndInstance { + a: u64, + b: u64, + _p: PhantomData, +} + +impl AndInstance { + // produces an AND instance + fn new() -> Self { + let mut rng = rand::thread_rng(); + let a: u64 = rng.gen(); + let b: u64 = rng.gen(); + Self { + a, + b, + _p: PhantomData, + } + } +} + +#[derive(Clone, Debug)] +struct AndCircuit { + batch: Vec>, +} + +impl AndCircuit { + // produces a batch of AND instances + fn new(num_ops_per_step: usize) -> Self { + let mut batch = Vec::new(); + for _ in 0..num_ops_per_step { + batch.push(AndInstance::new()); + } + Self { batch } + } +} + +pub fn u64_into_bit_vec_le>( + mut cs: CS, + value: Option, +) -> Result, SynthesisError> { + let values = match value { + Some(ref value) => { + let mut tmp = Vec::with_capacity(64); + + for i in 0..64 { + tmp.push(Some(*value >> i & 1 == 1)); + } + + tmp + } + None => vec![None; 64], + }; + + let bits = values + .into_iter() + .enumerate() + .map(|(i, b)| AllocatedBit::alloc(cs.namespace(|| format!("bit {}", i)), b)) + .collect::, SynthesisError>>()?; + + Ok(bits) +} + +/// Gets as input the little indian representation of a number and spits out the number +pub fn le_bits_to_num( + mut cs: CS, + bits: &[AllocatedBit], +) -> Result, SynthesisError> +where + Scalar: PrimeField + PrimeFieldBits, + CS: ConstraintSystem, +{ + // We loop over the input bits and construct the constraint + // and the field element that corresponds to the result + let mut lc = LinearCombination::zero(); + let mut coeff = Scalar::ONE; + let mut fe = Some(Scalar::ZERO); + for bit in bits.iter() { + lc = lc + (coeff, bit.get_variable()); + fe = bit.get_value().map(|val| { + if val { + fe.unwrap() + coeff + } else { + fe.unwrap() + } + }); + coeff = coeff.double(); + } + let num = AllocatedNum::alloc(cs.namespace(|| "Field element"), || { + fe.ok_or(SynthesisError::AssignmentMissing) + })?; + lc = lc - num.get_variable(); + cs.enforce(|| "compute number from bits", |lc| lc, |lc| lc, |_| lc); + Ok(num) +} + +impl StepCircuit for AndCircuit { + fn arity(&self) -> usize { + 1 + } + + fn synthesize>( + &self, + cs: &mut CS, + z_in: &[AllocatedNum], + ) -> Result>, SynthesisError> { + for i in 0..self.batch.len() { + // allocate a and b as field elements + let a = AllocatedNum::alloc(cs.namespace(|| format!("a_{}", i)), || { + Ok(G::Scalar::from(self.batch[i].a)) + })?; + let b = AllocatedNum::alloc(cs.namespace(|| format!("b_{}", i)), || { + Ok(G::Scalar::from(self.batch[i].b)) + })?; + + // obtain bit representations of a and b + let a_bits = u64_into_bit_vec_le( + cs.namespace(|| format!("a_bits_{}", i)), + Some(self.batch[i].a), + )?; // little endian + let b_bits = u64_into_bit_vec_le( + cs.namespace(|| format!("b_bits_{}", i)), + Some(self.batch[i].b), + )?; // little endian + + // enforce that bits of a and b are correct + let a_from_bits = le_bits_to_num(cs.namespace(|| format!("a_{}", i)), &a_bits)?; + let b_from_bits = le_bits_to_num(cs.namespace(|| format!("b_{}", i)), &b_bits)?; + + cs.enforce( + || format!("a_{} == a_from_bits", i), + |lc| lc + a.get_variable(), + |lc| lc + CS::one(), + |lc| lc + a_from_bits.get_variable(), + ); + cs.enforce( + || format!("b_{} == b_from_bits", i), + |lc| lc + b.get_variable(), + |lc| lc + CS::one(), + |lc| lc + b_from_bits.get_variable(), + ); + + let mut c_bits = Vec::new(); + + // perform bitwise AND + for i in 0..64 { + let c_bit = AllocatedBit::and( + cs.namespace(|| format!("and_bit_{}", i)), + &a_bits[i], + &b_bits[i], + )?; + c_bits.push(c_bit); + } + + // convert back to an allocated num + let c_from_bits = le_bits_to_num(cs.namespace(|| format!("c_{}", i)), &c_bits)?; + + let c = AllocatedNum::alloc(cs.namespace(|| format!("c_{}", i)), || { + Ok(G::Scalar::from(self.batch[i].a & self.batch[i].b)) + })?; + + // enforce that c is correct + cs.enforce( + || format!("c_{} == c_from_bits", i), + |lc| lc + c.get_variable(), + |lc| lc + CS::one(), + |lc| lc + c_from_bits.get_variable(), + ); + } + + Ok(z_in.to_vec()) + } +} + +/// cargo run --release --example and +fn main() { + println!("========================================================="); + println!("Nova-based 64-bit bitwise AND example"); + println!("========================================================="); + + let num_steps = 32; + for num_ops_per_step in [1024, 2048, 4096, 8192, 16384, 32768, 65536] { + // number of instances of AND per Nova's recursive step + let circuit_primary = AndCircuit::new(num_ops_per_step); + let circuit_secondary = TrivialCircuit::default(); + + println!( + "Proving {} AND ops ({num_ops_per_step} instances per step and {num_steps} steps)", + num_ops_per_step * num_steps + ); + + // produce public parameters + let start = Instant::now(); + println!("Producing public parameters..."); + let pp = PublicParams::< + E1, + E2, + AndCircuit<::GE>, + TrivialCircuit<::Scalar>, + >::setup( + &circuit_primary, + &circuit_secondary, + &*S1::ck_floor(), + &*S2::ck_floor(), + ); + println!("PublicParams::setup, took {:?} ", start.elapsed()); + + println!( + "Number of constraints per step (primary circuit): {}", + pp.num_constraints().0 + ); + println!( + "Number of constraints per step (secondary circuit): {}", + pp.num_constraints().1 + ); + + println!( + "Number of variables per step (primary circuit): {}", + pp.num_variables().0 + ); + println!( + "Number of variables per step (secondary circuit): {}", + pp.num_variables().1 + ); + + // produce non-deterministic advice + let circuits = (0..num_steps) + .map(|_| AndCircuit::new(num_ops_per_step)) + .collect::>(); + + type C1 = AndCircuit<::GE>; + type C2 = TrivialCircuit<::Scalar>; + + // produce a recursive SNARK + println!("Generating a RecursiveSNARK..."); + let mut recursive_snark: RecursiveSNARK = + RecursiveSNARK::::new( + &pp, + &circuits[0], + &circuit_secondary, + &[::Scalar::zero()], + &[::Scalar::zero()], + ) + .unwrap(); + + let start = Instant::now(); + for circuit_primary in circuits.iter() { + let res = recursive_snark.prove_step(&pp, circuit_primary, &circuit_secondary); + assert!(res.is_ok()); + } + println!( + "RecursiveSNARK::prove {} AND ops: took {:?} ", + num_ops_per_step * num_steps, + start.elapsed() + ); + + // verify the recursive SNARK + println!("Verifying a RecursiveSNARK..."); + let res = recursive_snark.verify( + &pp, + num_steps, + &[::Scalar::ZERO], + &[::Scalar::ZERO], + ); + println!("RecursiveSNARK::verify: {:?}", res.is_ok(),); + assert!(res.is_ok()); + + // produce a compressed SNARK + println!("Generating a CompressedSNARK using Spartan with multilinear KZG..."); + let (pk, vk) = CompressedSNARK::<_, _, _, _, S1, S2>::setup(&pp).unwrap(); + + let start = Instant::now(); + + let res = CompressedSNARK::<_, _, _, _, S1, S2>::prove(&pp, &pk, &recursive_snark); + println!( + "CompressedSNARK::prove: {:?}, took {:?}", + res.is_ok(), + start.elapsed() + ); + assert!(res.is_ok()); + let compressed_snark = res.unwrap(); + + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + bincode::serialize_into(&mut encoder, &compressed_snark).unwrap(); + let compressed_snark_encoded = encoder.finish().unwrap(); + println!( + "CompressedSNARK::len {:?} bytes", + compressed_snark_encoded.len() + ); + + // verify the compressed SNARK + println!("Verifying a CompressedSNARK..."); + let start = Instant::now(); + let res = compressed_snark.verify( + &vk, + num_steps, + &[::Scalar::ZERO], + &[::Scalar::ZERO], + ); + println!( + "CompressedSNARK::verify: {:?}, took {:?}", + res.is_ok(), + start.elapsed() + ); + assert!(res.is_ok()); + println!("========================================================="); + } +} diff --git a/src/circuit.rs b/src/circuit.rs index e613f4030..e50dfa8bb 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -464,8 +464,8 @@ mod tests { ¶ms2, ro_consts1, ro_consts2, - &expect!["9825"], - &expect!["10357"], + &expect!["9817"], + &expect!["10349"], ); } @@ -481,8 +481,8 @@ mod tests { ¶ms2, ro_consts1, ro_consts2, - &expect!["9993"], - &expect!["10546"], + &expect!["9985"], + &expect!["10538"], ); } @@ -498,8 +498,8 @@ mod tests { ¶ms2, ro_consts1, ro_consts2, - &expect!["10272"], - &expect!["10969"], + &expect!["10264"], + &expect!["10961"], ); } } diff --git a/src/constants.rs b/src/constants.rs index 084a615fa..3003b16eb 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -4,7 +4,7 @@ pub(crate) const NUM_CHALLENGE_BITS: usize = 128; pub(crate) const BN_LIMB_WIDTH: usize = 64; pub(crate) const BN_N_LIMBS: usize = 4; pub(crate) const NUM_FE_WITHOUT_IO_FOR_CRHF: usize = 17; -pub(crate) const NUM_FE_FOR_RO: usize = 24; +pub(crate) const NUM_FE_FOR_RO: usize = 9; /// Bit size of Nova field element hashes pub const NUM_HASH_BITS: usize = 250; diff --git a/src/gadgets/ecc.rs b/src/gadgets/ecc.rs index ea181240e..692f03916 100644 --- a/src/gadgets/ecc.rs +++ b/src/gadgets/ecc.rs @@ -193,7 +193,7 @@ where } /// Adds other point to this point and returns the result. Assumes that the two points are - /// different and that both `other.is_infinity` and `this.is_infinty` are bits + /// different and that both `other.is_infinity` and `this.is_infinity` are bits pub fn add_internal>( &self, mut cs: CS, diff --git a/src/gadgets/r1cs.rs b/src/gadgets/r1cs.rs index 24a53542e..2fd5c6b24 100644 --- a/src/gadgets/r1cs.rs +++ b/src/gadgets/r1cs.rs @@ -239,8 +239,10 @@ impl AllocatedRelaxedR1CSInstance { // Compute r: let mut ro = E::ROCircuit::new(ro_consts, NUM_FE_FOR_RO); ro.absorb(params); - self.absorb_in_ro(cs.namespace(|| "absorb running instance"), &mut ro)?; + + // running instance `U` does not need to absorbed since u.X[0] = Hash(params, U, i, z0, zi) u.absorb_in_ro(&mut ro); + ro.absorb(&T.x); ro.absorb(&T.y); ro.absorb(&T.is_infinity); diff --git a/src/lib.rs b/src/lib.rs index 5df24530d..6e066d03b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1095,13 +1095,13 @@ mod tests { test_pp_digest_with::, EE<_>>( &trivial_circuit1, &trivial_circuit2, - &expect!["f4a04841515b4721519e2671b7ee11e58e2d4a30bb183ded963b71ad2ec80d00"], + &expect!["492fd902cd7174159bc9a6f827d92eb54ff25efa9d0673dffdb0efd02995df01"], ); test_pp_digest_with::, EE<_>>( &cubic_circuit1, &trivial_circuit2, - &expect!["dc1b7c40ab50c5c6877ad027769452870cc28f1d13f140de7ca3a00138c58502"], + &expect!["9b0701d9422658e3f74a85ab3e485c06f3ecca9c2b1800aab80004034d754f01"], ); let trivial_circuit1_grumpkin = TrivialCircuit::<::Scalar>::default(); @@ -1114,22 +1114,22 @@ mod tests { test_pp_digest_with::, EE<_>>( &trivial_circuit1_grumpkin, &trivial_circuit2_grumpkin, - &expect!["2af2a80ea8a0c21cfc4096e9b2d4822344a29174d88cd86239a99a363efe8702"], + &expect!["1267235eb3d139e466dd9c814eaf73f01b063ccb4cad04848c0eb62f079a9601"], ); test_pp_digest_with::, EE<_>>( &cubic_circuit1_grumpkin, &trivial_circuit2_grumpkin, - &expect!["9273fd39ed6220ea28e60abca8bdb3180f90e37e6aaf3031e6d670d073e8a002"], + &expect!["57afac2edd20d39b202151906e41154ba186c9dde497448d1332dc6de2f76302"], ); test_pp_digest_with::, EE<_>>( &trivial_circuit1_grumpkin, &trivial_circuit2_grumpkin, - &expect!["eac63861dcbaa924a1bd9721f01528271385dd6ff182c77bc62e03448adc1902"], + &expect!["070d247d83e17411d65c12260980ebcc59df88d3882d84eb62e6ab466e381503"], ); test_pp_digest_with::, EE<_>>( &cubic_circuit1_grumpkin, &trivial_circuit2_grumpkin, - &expect!["21bd7e07175b23e3bd2bbc6a0c3b9efd8603815609dbe93f8df9c174e132de03"], + &expect!["47c2caa008323b588b47ab8b6c0e94f980599188abe117c4d21ffff81494f303"], ); let trivial_circuit1_secp = TrivialCircuit::<::Scalar>::default(); @@ -1139,12 +1139,12 @@ mod tests { test_pp_digest_with::, EE<_>>( &trivial_circuit1_secp, &trivial_circuit2_secp, - &expect!["fd2ff9d03e5512c66014399352ce0a8d722fe9c95005067ca05fe5ce84aabb02"], + &expect!["04b5d1798be6d74b3701390b87078e70ebf3ddaad80c375319f320cedf8bca00"], ); test_pp_digest_with::, EE<_>>( &cubic_circuit1_secp, &trivial_circuit2_secp, - &expect!["1e4526ea166c8cccb685bd065ba82438b3ba89e24612d3e071fd30a6065c2b03"], + &expect!["346b5f27cf24c79386f4de7a8bfb58970181ae7f0de7d2e3f10ad5dfd8fc2302"], ); } diff --git a/src/nifs.rs b/src/nifs.rs index 9925b3330..97d40fd06 100644 --- a/src/nifs.rs +++ b/src/nifs.rs @@ -31,6 +31,12 @@ impl NIFS { /// a folded Relaxed R1CS instance-witness tuple `(U, W)` of the same shape `shape`, /// with the guarantee that the folded witness `W` satisfies the folded instance `U` /// if and only if `W1` satisfies `U1` and `W2` satisfies `U2`. + /// + /// Note that this code is tailored for use with Nova's IVC scheme, which enforces + /// certain requirements between the two instances that are folded. + /// In particular, it requires that `U1` and `U2` are such that the hash of `U1` is stored in the public IO of `U2`. + /// In this particular setting, this means that if `U2` is absorbed in the RO, it implicitly absorbs `U1` as well. + /// So the code below avoids absorbing `U1` in the RO. #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip_all, level = "trace", name = "NIFS::prove")] pub fn prove( @@ -49,8 +55,7 @@ impl NIFS { // append the digest of pp to the transcript ro.absorb(scalar_as_base::(*pp_digest)); - // append U1 and U2 to transcript - U1.absorb_in_ro(&mut ro); + // append U2 to transcript, U1 does not need to absorbed since U2.X[0] = Hash(params, U1, i, z0, zi) U2.absorb_in_ro(&mut ro); // compute a commitment to the cross-term @@ -103,8 +108,7 @@ impl NIFS { // append the digest of pp to the transcript ro.absorb(scalar_as_base::(*pp_digest)); - // append U1 and U2 to transcript - U1.absorb_in_ro(&mut ro); + // append U2 to transcript, U1 does not need to absorbed since U2.X[0] = Hash(params, U1, i, z0, zi) U2.absorb_in_ro(&mut ro); // compute a commitment to the cross-term @@ -146,8 +150,7 @@ impl NIFS { // append the digest of pp to the transcript ro.absorb(scalar_as_base::(*pp_digest)); - // append U1 and U2 to transcript - U1.absorb_in_ro(&mut ro); + // append U2 to transcript, U1 does not need to absorbed since U2.X[0] = Hash(params, U1, i, z0, zi) U2.absorb_in_ro(&mut ro); // append `comm_T` to the transcript and obtain a challenge diff --git a/src/provider/traits.rs b/src/provider/traits.rs index caea6a747..5a1d83f52 100644 --- a/src/provider/traits.rs +++ b/src/provider/traits.rs @@ -29,7 +29,7 @@ pub trait DlogGroup: /// Produce a vector of group elements using a static label fn from_label(label: &'static [u8], n: usize) -> Vec; - /// Returns the affine coordinates (x, y, infinty) for the point + /// Returns the affine coordinates (x, y, infinity) for the point fn to_coordinates(&self) -> (::Base, ::Base, bool); } diff --git a/src/r1cs/mod.rs b/src/r1cs/mod.rs index e2772ce86..655403608 100644 --- a/src/r1cs/mod.rs +++ b/src/r1cs/mod.rs @@ -240,14 +240,13 @@ impl R1CSShape { } // Checks regularity conditions on the R1CSShape, required in Spartan-class SNARKs - // Returns false if num_cons, num_vars, or num_io are not powers of two, or if num_io > num_vars + // Returns false if num_cons or num_vars are not powers of two, or if num_io > num_vars #[inline] pub(crate) fn is_regular_shape(&self) -> bool { let cons_valid = self.num_cons.next_power_of_two() == self.num_cons; let vars_valid = self.num_vars.next_power_of_two() == self.num_vars; - let io_valid = self.num_io.next_power_of_two() == self.num_io; let io_lt_vars = self.num_io < self.num_vars; - cons_valid && vars_valid && io_valid && io_lt_vars + cons_valid && vars_valid && io_lt_vars } pub(crate) fn multiply_vec( @@ -513,17 +512,17 @@ impl R1CSShape { Ok(CE::::commit(ck, T)) } - /// Pads the `R1CSShape` so that the number of variables is a power of two + /// Pads the `R1CSShape` so that the shape passes `is_regular_shape` /// Renumbers variables to accommodate padded variables pub fn pad(&self) -> Self { - // equalize the number of variables and constraints - let m = max(self.num_vars, self.num_cons).next_power_of_two(); - // check if the provided R1CSShape is already as required - if self.num_vars == m && self.num_cons == m { + if self.is_regular_shape() { return self.clone(); } + // equalize the number of variables, constraints, and public IO + let m = max(max(self.num_vars, self.num_cons), self.num_io).next_power_of_two(); + // check if the number of variables are as expected, then // we simply set the number of constraints to the next power of two if self.num_vars == m { diff --git a/src/supernova/circuit.rs b/src/supernova/circuit.rs index 9e357e298..3a1042725 100644 --- a/src/supernova/circuit.rs +++ b/src/supernova/circuit.rs @@ -833,8 +833,8 @@ mod tests { ¶ms2, ro_consts1, ro_consts2, - &expect!["9844"], - &expect!["10392"], + &expect!["9836"], + &expect!["10384"], 1, ); // TODO: extend to num_augmented_circuits >= 2 @@ -852,8 +852,8 @@ mod tests { ¶ms2, ro_consts1, ro_consts2, - &expect!["10012"], - &expect!["10581"], + &expect!["10004"], + &expect!["10573"], 1, ); // TODO: extend to num_augmented_circuits >= 2 @@ -871,8 +871,8 @@ mod tests { ¶ms2, ro_consts1, ro_consts2, - &expect!["10291"], - &expect!["11004"], + &expect!["10283"], + &expect!["10996"], 1, ); // TODO: extend to num_augmented_circuits >= 2 diff --git a/src/supernova/test.rs b/src/supernova/test.rs index de50d5f8d..569dc2646 100644 --- a/src/supernova/test.rs +++ b/src/supernova/test.rs @@ -464,8 +464,8 @@ fn test_recursive_circuit_with( secondary_params: &SuperNovaAugmentedCircuitParams, ro_consts1: ROConstantsCircuit, ro_consts2: ROConstantsCircuit, - num_constraints_primary: usize, - num_constraints_secondary: usize, + num_constraints_primary: &Expect, + num_constraints_secondary: &Expect, ) where E1: Engine::Scalar>, E2: Engine::Scalar>, @@ -480,7 +480,7 @@ fn test_recursive_circuit_with( panic!("{}", e) } let (shape1, ck1) = cs.r1cs_shape_and_key(&*default_ck_hint()); - assert_eq!(cs.num_constraints(), num_constraints_primary); + num_constraints_primary.assert_eq(&cs.num_constraints().to_string()); // Initialize the shape and ck for the secondary let step_circuit2 = TrivialSecondaryCircuit::default(); @@ -498,7 +498,7 @@ fn test_recursive_circuit_with( panic!("{}", e) } let (shape2, ck2) = cs.r1cs_shape_and_key(&*default_ck_hint()); - assert_eq!(cs.num_constraints(), num_constraints_secondary); + num_constraints_secondary.assert_eq(&cs.num_constraints().to_string()); // Execute the base case for the primary let zero1 = <::Base as Field>::ZERO; @@ -565,7 +565,12 @@ fn test_recursive_circuit() { let ro_consts2: ROConstantsCircuit = PoseidonConstantsCircuit::default(); test_recursive_circuit_with::( - ¶ms1, ¶ms2, ro_consts1, ro_consts2, 9844, 12025, + ¶ms1, + ¶ms2, + ro_consts1, + ro_consts2, + &expect!["9836"], + &expect!["12017"], ); } @@ -613,7 +618,7 @@ fn test_supernova_pp_digest() { test_pp_digest_with::( &test_rom, - &expect!["7e203fdfeab0ee8f56f8948497f8de73539d52e64cef89e44fff84711cf8b100"], + &expect!["95f57227c5d62d13b9fe55deac13b8bd099b068bcc785d7b3a054bf376f68e00"], ); let rom = vec![ @@ -628,7 +633,7 @@ fn test_supernova_pp_digest() { test_pp_digest_with::( &test_rom_grumpkin, - &expect!["5caf6efbdb5a928b44a6eb4ff597e2b5f6764450ceb86b7065aac6cf965c0203"], + &expect!["d439e957618eb071360f9c87c0014fd0cfa21f1271813004d18f967355912a01"], ); let rom = vec![ @@ -643,7 +648,7 @@ fn test_supernova_pp_digest() { test_pp_digest_with::( &test_rom_secp, - &expect!["e955513a59f75c63bc0649425045e6e472bddf4490a558e95bfcab14b4911a00"], + &expect!["5dfc2cc21f0a29a67ec3b3cbb7fbff535c876ef51e655f4abf4c00e058175103"], ); } diff --git a/src/traits/commitment.rs b/src/traits/commitment.rs index 5bc298a82..cb97eac61 100644 --- a/src/traits/commitment.rs +++ b/src/traits/commitment.rs @@ -56,7 +56,7 @@ pub trait CommitmentTrait: fn decompress(c: &Self::CompressedCommitment) -> Result; } -/// A trait that helps determine the lenght of a structure. +/// A trait that helps determine the length of a structure. /// Note this does not impose any memory representation contraints on the structure. pub trait Len { /// Returns the length of the structure.