diff --git a/crates/halo2_proofs/src/arithmetic.rs b/crates/halo2_proofs/src/arithmetic.rs index bfb37fc1..c4e57b3a 100644 --- a/crates/halo2_proofs/src/arithmetic.rs +++ b/crates/halo2_proofs/src/arithmetic.rs @@ -218,7 +218,11 @@ impl BoothBuckets { /// Performs a small multi-exponentiation operation. /// Uses the double-and-add algorithm with doublings shared across points. +/// +/// This function will panic if coeffs and bases have a different length. pub fn small_multiexp(coeffs: &[C::Scalar], bases: &[C]) -> C::Curve { + assert_eq!(coeffs.len(), bases.len()); + let coeffs: Vec<_> = coeffs.iter().map(|a| a.to_repr()).collect(); let mut acc = C::Curve::identity(); @@ -790,6 +794,39 @@ fn test_multiexp() { } } +#[test] +fn test_small_multiexp() { + let mut rng = rng(); + for len in [0, 1, 2, 3, 4] { + let coeffs = (0..len).map(|_| Fq::random(&mut rng)).collect::>(); + let bases = (0..len) + .map(|_| EpAffine::from(Ep::random(&mut rng))) + .collect::>(); + + let expected = coeffs + .iter() + .zip(&bases) + .map(|(coeff, base)| *base * coeff) + .fold(Ep::identity(), |acc, val| acc + val); + + assert_eq!(small_multiexp(&coeffs, &bases), expected); + } +} + +#[test] +#[should_panic(expected = "left == right")] +fn test_small_multiexp_rejects_extra_base() { + let base = EpAffine::from(Ep::generator()); + small_multiexp(&[Fq::ONE], &[base, base]); +} + +#[test] +#[should_panic(expected = "left == right")] +fn test_small_multiexp_rejects_missing_base() { + let base = EpAffine::from(Ep::generator()); + small_multiexp(&[Fq::ONE, Fq::ZERO], &[base]); +} + #[test] fn test_booth_digit_boundaries() { let assert_digit = |bytes: &[u8], window_bits, window, magnitude, negative| { diff --git a/crates/halo2_proofs/src/poly.rs b/crates/halo2_proofs/src/poly.rs index 46333d10..c42a3087 100644 --- a/crates/halo2_proofs/src/poly.rs +++ b/crates/halo2_proofs/src/poly.rs @@ -182,7 +182,10 @@ impl Polynomial, LagrangeCoeff> { impl<'a, F: Field, B: Basis> Add<&'a Polynomial> for Polynomial { type Output = Polynomial; + /// This function will panic if the operands have different lengths, + /// i.e. were created for different domain sizes. fn add(mut self, rhs: &'a Polynomial) -> Polynomial { + assert_eq!(self.values.len(), rhs.values.len()); parallelize(&mut self.values, |lhs, start| { for (lhs, rhs) in lhs.iter_mut().zip(rhs.values[start..].iter()) { *lhs += *rhs; @@ -315,6 +318,38 @@ mod tests { use super::{EvaluationDomain, Rotation}; + #[test] + fn test_polynomial_addition() { + let domain = EvaluationDomain::::new(1, 2); + let lhs = domain.coeff_from_vec([1, 2, 3, 4].map(pallas::Base::from).to_vec()); + let rhs = domain.coeff_from_vec([10, 20, 30, 40].map(pallas::Base::from).to_vec()); + + let sum = lhs + &rhs; + assert_eq!(&sum[..], [11, 22, 33, 44].map(pallas::Base::from)); + } + + #[test] + #[should_panic(expected = "left == right")] + fn test_polynomial_addition_rejects_larger_rhs() { + let small_domain = EvaluationDomain::::new(1, 1); + let large_domain = EvaluationDomain::::new(1, 2); + let small = small_domain.coeff_from_vec([1, 2].map(pallas::Base::from).to_vec()); + let large = large_domain.coeff_from_vec([10, 20, 30, 40].map(pallas::Base::from).to_vec()); + + let _ = small + &large; + } + + #[test] + #[should_panic(expected = "left == right")] + fn test_polynomial_addition_rejects_smaller_rhs() { + let small_domain = EvaluationDomain::::new(1, 1); + let large_domain = EvaluationDomain::::new(1, 2); + let small = small_domain.coeff_from_vec([1, 2].map(pallas::Base::from).to_vec()); + let large = large_domain.coeff_from_vec([10, 20, 30, 40].map(pallas::Base::from).to_vec()); + + let _ = large + &small; + } + #[test] fn test_copy_rotated_chunk() { let k = 11; diff --git a/crates/halo2_proofs/src/poly/domain.rs b/crates/halo2_proofs/src/poly/domain.rs index af48cc41..aac29fea 100644 --- a/crates/halo2_proofs/src/poly/domain.rs +++ b/crates/halo2_proofs/src/poly/domain.rs @@ -861,7 +861,16 @@ impl> EvaluationDomain { let common = (xn - F::ONE) * self.barycentric_weight; for (rotation, result) in rotations.into_iter().zip(results.iter_mut()) { let rotation = Rotation(rotation); - *result = self.rotate_omega(*result * common, rotation); + *result = if result.is_zero_vartime() { + // The denominator `x - omega^i` was zero and survived batch + // inversion as zero: `x` is this basis polynomial's own + // domain point, so the shared numerator `xn - 1` vanishes + // too and the removable singularity cancels to + // `l_i(omega^i) = 1`. + F::ONE + } else { + self.rotate_omega(*result * common, rotation) + }; } results @@ -1331,6 +1340,41 @@ fn test_l_i() { } } +#[test] +fn test_l_i_at_domain_points() { + use crate::pasta::pallas::Scalar; + let domain = EvaluationDomain::::new(1, 3); + + for j in 0..8u64 { + let x = domain.get_omega().pow([j]); + let xn = x.pow([8]); + assert_eq!(xn, Scalar::ONE); + + // Rotations are periodic modulo the domain size, so every rotation + // in -7..=7 congruent to j (two of them, except for j = 0) must + // evaluate to one, and every other rotation to zero. + let evaluations = domain.l_i_range(x, xn, -7i32..=7); + for (evaluation, rotation) in evaluations.iter().zip(-7i32..=7) { + let expected = if rotation.rem_euclid(8) as u64 == j { + Scalar::ONE + } else { + Scalar::ZERO + }; + assert_eq!(*evaluation, expected, "rotation {rotation} at omega^{j}"); + } + } + + // A domain point whose own basis polynomial is not requested makes + // every requested basis polynomial evaluate to zero. + let x = domain.get_omega().pow([5]); + let evaluations = domain.l_i_range(x, Scalar::ONE, 0..=3); + assert!( + evaluations + .iter() + .all(|evaluation| *evaluation == Scalar::ZERO) + ); +} + #[test] fn test_copy_rotated_chunk_extended() { use pasta_curves::pallas; diff --git a/crates/halo2_proofs/src/poly/multiopen/prover.rs b/crates/halo2_proofs/src/poly/multiopen/prover.rs index f1f004f5..0ec9cf55 100644 --- a/crates/halo2_proofs/src/poly/multiopen/prover.rs +++ b/crates/halo2_proofs/src/poly/multiopen/prover.rs @@ -250,10 +250,15 @@ fn collapse_polynomials( /// Create a multi-opening proof. /// +/// A queried polynomial with fewer coefficients than the parameters' domain +/// size is treated as zero-extended to that size. +/// /// # Errors /// -/// Returns [`std::io::ErrorKind::InvalidInput`] if `queries` is empty or -/// contains more than one query for the same commitment at the same point. +/// Returns [`std::io::ErrorKind::InvalidInput`] if `queries` is empty, +/// contains more than one query for the same commitment at the same point, +/// or queries a polynomial with more coefficients than the parameters' +/// domain size. pub fn create_proof< 'a, I, @@ -286,11 +291,24 @@ where let mut q_blinds = vec![Blind(C::Scalar::ZERO); point_sets.len()]; for commitment_data in poly_map { let set_index = commitment_data.set_index; + if commitment_data.commitment.poly.num_coeffs() > params.n as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "query polynomial has more coefficients than the parameters' domain size", + )); + } polynomial_groups[set_index].push(commitment_data.commitment.poly); q_blinds[set_index] *= *x_1; q_blinds[set_index] += commitment_data.commitment.blind; } - let q_polys = collapse_polynomials(&polynomial_groups, *x_1); + let mut q_polys = collapse_polynomials(&polynomial_groups, *x_1); + // Each collapsed polynomial keeps its group head's length. Zero-extend to + // the parameters' length — absent high coefficients read as zero, exactly + // as the polynomial's commitment treats them — so the final combination + // below folds equal-length operands. + for q_poly in &mut q_polys { + q_poly.values.resize(params.n as usize, C::Scalar::ZERO); + } let q_prime_poly = point_sets .iter() @@ -386,23 +404,36 @@ mod tests { use std::fmt::Debug; use std::marker::PhantomData; + // A group's collapse keeps the first polynomial's length: shorter group + // members are zero-extended and longer ones truncated to it, matching + // `fold_polynomial_range`'s treatment of absent coefficients. Polynomial + // addition itself requires equal lengths, so fold each coefficient + // directly. fn reference_collapse( groups: &[Vec<&Polynomial>], challenge: F, ) -> Vec> { groups .iter() - .map(|group| { - group[1..] - .iter() - .fold(group[0].clone(), |accumulator, polynomial| { - accumulator * challenge + polynomial + .map(|group| Polynomial { + values: (0..group[0].values.len()) + .map(|coefficient_index| { + group.iter().fold(F::ZERO, |accumulator, polynomial| { + accumulator * challenge + + polynomial + .values + .get(coefficient_index) + .copied() + .unwrap_or(F::ZERO) + }) }) + .collect(), + _marker: PhantomData, }) .collect() } - fn streaming_collapse_matches_operator_collapse() + fn streaming_collapse_matches_reference() where F: Field + From + Debug, { @@ -462,12 +493,125 @@ mod tests { } #[test] - fn streaming_collapse_matches_operator_collapse_fp() { - streaming_collapse_matches_operator_collapse::(); + fn streaming_collapse_matches_reference_fp() { + streaming_collapse_matches_reference::(); + } + + #[test] + fn streaming_collapse_matches_reference_fq() { + streaming_collapse_matches_reference::(); + } + + #[test] + fn short_query_polynomial_proves_and_verifies() { + use crate::arithmetic::eval_polynomial; + use crate::pasta::EpAffine; + use crate::poly::commitment::{Blind, Params}; + use crate::poly::multiopen::{ProverQuery, VerifierQuery, create_proof, verify_proof}; + use crate::transcript::{ + Blake2bRead, Blake2bWrite, Challenge255, TranscriptRead, TranscriptWrite, + }; + use group::Curve; + use rand::rng; + + let mut rng = rng(); + let params = Params::::new(1); + + // A one-coefficient query polynomial is zero-extended to the + // parameters' length, so its commitment is that of the padded copy. + let short = Polynomial:: { + values: vec![Fq::from(5)], + _marker: PhantomData, + }; + let padded = Polynomial:: { + values: vec![Fq::from(5), Fq::ZERO], + _marker: PhantomData, + }; + let full = Polynomial:: { + values: vec![Fq::from(3), Fq::from(4)], + _marker: PhantomData, + }; + let short_blind = Blind(Fq::random(&mut rng)); + let full_blind = Blind(Fq::random(&mut rng)); + let short_commitment = params.commit(&padded, short_blind).to_affine(); + let full_commitment = params.commit(&full, full_blind).to_affine(); + + let short_point = Fq::from(97); + let full_point = Fq::from(43); + let short_eval = eval_polynomial(&short[..], short_point); + let full_eval = eval_polynomial(&full[..], full_point); + + let mut transcript = + Blake2bWrite::, EpAffine, Challenge255>::init(vec![]); + transcript.write_point(short_commitment).unwrap(); + transcript.write_point(full_commitment).unwrap(); + transcript.write_scalar(short_eval).unwrap(); + transcript.write_scalar(full_eval).unwrap(); + create_proof( + ¶ms, + rng, + &mut transcript, + vec![ + ProverQuery { + point: short_point, + poly: &short, + blind: short_blind, + }, + ProverQuery { + point: full_point, + poly: &full, + blind: full_blind, + }, + ], + ) + .unwrap(); + let proof = transcript.finalize(); + + let mut transcript = + Blake2bRead::<&[u8], EpAffine, Challenge255>::init(&proof[..]); + assert_eq!(transcript.read_point().unwrap(), short_commitment); + assert_eq!(transcript.read_point().unwrap(), full_commitment); + assert_eq!(transcript.read_scalar().unwrap(), short_eval); + assert_eq!(transcript.read_scalar().unwrap(), full_eval); + let guard = verify_proof( + ¶ms, + &mut transcript, + vec![ + VerifierQuery::new_commitment(&short_commitment, short_point, short_eval), + VerifierQuery::new_commitment(&full_commitment, full_point, full_eval), + ], + params.empty_msm(), + ) + .unwrap(); + assert!(guard.use_challenges().eval()); } #[test] - fn streaming_collapse_matches_operator_collapse_fq() { - streaming_collapse_matches_operator_collapse::(); + fn oversized_query_polynomial_is_rejected() { + use crate::pasta::EpAffine; + use crate::poly::commitment::{Blind, Params}; + use crate::poly::multiopen::{ProverQuery, create_proof}; + use crate::transcript::{Blake2bWrite, Challenge255}; + use rand::rng; + + let params = Params::::new(1); + let oversized = Polynomial:: { + values: vec![Fq::ONE; 4], + _marker: PhantomData, + }; + + let mut transcript = + Blake2bWrite::, EpAffine, Challenge255>::init(vec![]); + let result = create_proof( + ¶ms, + rng(), + &mut transcript, + vec![ProverQuery { + point: Fq::from(97), + poly: &oversized, + blind: Blind(Fq::ZERO), + }], + ); + assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidInput); } } diff --git a/crates/pasta_curves/src/arithmetic/curves.rs b/crates/pasta_curves/src/arithmetic/curves.rs index 56c978ed..57323cd6 100644 --- a/crates/pasta_curves/src/arithmetic/curves.rs +++ b/crates/pasta_curves/src/arithmetic/curves.rs @@ -51,6 +51,16 @@ pub trait CurveExt: /// /// This method is suitable for use as a random oracle. /// + /// # Panics + /// + /// The returned closure panics when invoked if the encoded + /// domain-separation tag, + /// `domain_prefix || "-" || Self::CURVE_ID || "_XMD:BLAKE2b_SSWU_RO_"`, + /// is longer than 255 bytes — that is, if `domain_prefix` is longer + /// than `233 - Self::CURVE_ID.len()` bytes (227 bytes for Pallas, + /// 228 bytes for Vesta). Constructing the hasher does not validate + /// `domain_prefix`; the panic occurs on the first message. + /// /// # Example /// /// ``` @@ -135,8 +145,10 @@ pub trait CurveExt: /// /// `input` and `output` must have the same power-of-two length, equal to /// `2^log_n`. The transform is unnormalized. Implementations return - /// `true` after writing the transform to `output`; the default returns - /// `false` without modifying `output`. + /// `true` after writing the transform to `output`, and return `false` + /// without modifying `output` for inputs they do not support — for + /// example, when `omega` does not have exact multiplicative order + /// `2^log_n`. The default returns `false` for every input. /// /// # Security /// diff --git a/crates/pasta_curves/src/curves.rs b/crates/pasta_curves/src/curves.rs index f3a538c4..6cfa1a61 100644 --- a/crates/pasta_curves/src/curves.rs +++ b/crates/pasta_curves/src/curves.rs @@ -69,8 +69,7 @@ macro_rules! impl_batch_mul_same_scalar_vartime { omega: Self::ScalarExt, log_n: u32, ) -> bool { - crate::glv::fft_vartime(input, output, omega, log_n); - true + crate::glv::fft_vartime(input, output, omega, log_n) } }; (native, $name:ident) => {}; diff --git a/crates/pasta_curves/src/glv.rs b/crates/pasta_curves/src/glv.rs index 854f32df..d46a5036 100644 --- a/crates/pasta_curves/src/glv.rs +++ b/crates/pasta_curves/src/glv.rs @@ -2834,12 +2834,17 @@ impl Decomposed { /// twiddle once, batches the Eisenstein tables and affine ladders for all /// nontrivial scalar multiplications in a layer, and batch-inverts the shared /// denominator for each layer's affine butterflies. +/// +/// The fused 8- and 16-point layers use factorization identities that hold +/// only when `omega` has exact multiplicative order `2^log_n`. If it does +/// not, this returns `false` without modifying `output` so the caller can +/// fall back to a generic implementation. pub(crate) fn fft_vartime( input: &[C], output: &mut [C::AffineExt], omega: C::ScalarExt, log_n: u32, -) { +) -> bool { fn bitreverse(mut value: usize, bits: usize) -> usize { let mut reversed = 0; for _ in 0..bits { @@ -2851,6 +2856,22 @@ pub(crate) fn fft_vartime( assert_eq!(input.len(), output.len()); assert_eq!(input.len(), 1usize << log_n); + + // `omega` has exact order `2^log_n` iff `omega^(2^(log_n - 1)) == -1` + // (for `log_n == 0`, iff `omega == 1`). + let exact_order = if log_n == 0 { + omega == C::ScalarExt::ONE + } else { + let mut half_order_power = omega; + for _ in 0..log_n - 1 { + half_order_power = half_order_power.square(); + } + half_order_power == -C::ScalarExt::ONE + }; + if !exact_order { + return false; + } + C::batch_normalize(input, output); // If one layer starts without identities and every `x_R - x_L` is // nonzero, both `L + R` and `L - R` are nonidentity. Carry that invariant @@ -2987,6 +3008,7 @@ pub(crate) fn fft_vartime( chunk *= 2; twiddle_stride /= 2; } + true } /// Replaces four radix-2 layers with a 16-point codelet that uses 14 scalar @@ -5170,7 +5192,7 @@ mod tests { let mut expected = input.clone(); reference(&mut expected, omega, log_n); let mut actual = alloc::vec![C::AffineExt::identity(); n]; - fft_vartime(&input, &mut actual, omega, log_n); + assert!(fft_vartime(&input, &mut actual, omega, log_n)); assert!( actual .iter() @@ -5181,6 +5203,47 @@ mod tests { } } + /// The affine FFT declines any `omega` whose multiplicative order is not + /// exactly `2^log_n` without modifying the output, including at the 8- + /// and 16-point fused-codelet thresholds whose factorizations require a + /// primitive root. + fn affine_fft_declines_non_exact_order_omega() { + for log_n in [1, 2, 3, 4, 5] { + let n = 1usize << log_n; + let mut omega = C::ScalarExt::ROOT_OF_UNITY_INV; + for _ in log_n..C::ScalarExt::S { + omega = omega.square(); + } + + let generator = C::generator(); + let input: Vec = (0..n) + .map(|i| generator * C::ScalarExt::from(i as u64 + 1)) + .collect(); + let untouched = alloc::vec![C::AffineExt::from(generator); n]; + + // A root of exact order `2^(log_n - 1)`, a higher-order root of + // exact order `2^(log_n + 1)`, and `omega = 1` must all be + // declined; only the exact-order root is accepted. + for bad_omega in [omega.square(), C::ScalarExt::ONE] { + let mut output = untouched.clone(); + assert!(!fft_vartime(&input, &mut output, bad_omega, log_n)); + assert_eq!(output, untouched, "a declined FFT must not write"); + } + if log_n < C::ScalarExt::S { + let mut higher_order = C::ScalarExt::ROOT_OF_UNITY_INV; + for _ in log_n + 1..C::ScalarExt::S { + higher_order = higher_order.square(); + } + let mut output = untouched.clone(); + assert!(!fft_vartime(&input, &mut output, higher_order, log_n)); + assert_eq!(output, untouched, "a declined FFT must not write"); + } + + let mut output = untouched.clone(); + assert!(fft_vartime(&input, &mut output, omega, log_n)); + } + } + fn batch_affine_buckets_match_native() { let generator = C::generator(); let two = generator.double(); @@ -5329,6 +5392,10 @@ mod tests { affine_fft_matches_projective::<$curve>(); } #[test] + fn affine_fft_decline() { + affine_fft_declines_non_exact_order_omega::<$curve>(); + } + #[test] fn batch_affine_buckets() { batch_affine_buckets_match_native::<$curve>(); } diff --git a/crates/pasta_curves/src/pallas.rs b/crates/pasta_curves/src/pallas.rs index 3b4700e9..6f61fe8c 100644 --- a/crates/pasta_curves/src/pallas.rs +++ b/crates/pasta_curves/src/pallas.rs @@ -215,3 +215,26 @@ fn test_hash_to_curve() { assert!(bool::from(p.is_on_curve())); assert!(bool::from(p.is_identity())); } + +// "pallas" is 6 bytes, so the longest domain prefix whose encoded +// domain-separation tag fits in 255 bytes is 227 bytes. +#[cfg(feature = "alloc")] +#[test] +fn test_hash_to_curve_domain_prefix_length_bound() { + use crate::arithmetic::CurveExt; + + let domain_prefix = "x".repeat(227); + let hash = Point::hash_to_curve(&domain_prefix); + assert!(bool::from(hash(b"message").is_on_curve())); +} + +#[cfg(feature = "alloc")] +#[test] +#[should_panic(expected = "22 + curve_id.len() + domain_prefix.len()")] +fn test_hash_to_curve_domain_prefix_too_long_panics() { + use crate::arithmetic::CurveExt; + + let domain_prefix = "x".repeat(228); + let hash = Point::hash_to_curve(&domain_prefix); + let _ = hash(b"message"); +} diff --git a/crates/pasta_curves/src/vesta.rs b/crates/pasta_curves/src/vesta.rs index be75c337..2383aafb 100644 --- a/crates/pasta_curves/src/vesta.rs +++ b/crates/pasta_curves/src/vesta.rs @@ -89,3 +89,26 @@ fn test_hash_to_curve() { format!("{:?}", z) == "0x1b58d4aa4d68c3f4d9916b77c79ff9911597a27f2ee46244e98eb9615172d2ad" ); } + +// "vesta" is 5 bytes, so the longest domain prefix whose encoded +// domain-separation tag fits in 255 bytes is 228 bytes. +#[cfg(feature = "alloc")] +#[test] +fn test_hash_to_curve_domain_prefix_length_bound() { + use crate::arithmetic::CurveExt; + + let domain_prefix = "x".repeat(228); + let hash = Point::hash_to_curve(&domain_prefix); + assert!(bool::from(hash(b"message").is_on_curve())); +} + +#[cfg(feature = "alloc")] +#[test] +#[should_panic(expected = "22 + curve_id.len() + domain_prefix.len()")] +fn test_hash_to_curve_domain_prefix_too_long_panics() { + use crate::arithmetic::CurveExt; + + let domain_prefix = "x".repeat(229); + let hash = Point::hash_to_curve(&domain_prefix); + let _ = hash(b"message"); +} diff --git a/crates/zcash_primitives/src/merkle_tree.rs b/crates/zcash_primitives/src/merkle_tree.rs index 46535f1d..0a2ade4c 100644 --- a/crates/zcash_primitives/src/merkle_tree.rs +++ b/crates/zcash_primitives/src/merkle_tree.rs @@ -238,15 +238,18 @@ pub fn write_incremental_witness( pub fn merkle_path_from_slice( mut witness: &[u8], ) -> io::Result> { - // Skip the first byte, which should be DEPTH to signify the length of - // the following vector of Pedersen hashes. - if witness[0] != DEPTH { + // The first byte should be DEPTH, the length of the following vector of + // Pedersen hashes. + let (&encoded_depth, rest) = witness + .split_first() + .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "missing Merkle path depth"))?; + if encoded_depth != DEPTH { return Err(io::Error::new( io::ErrorKind::InvalidData, "depth is not as expected", )); } - witness = &witness[1..]; + witness = rest; // Begin to construct the authentication path let (chunks, remainder) = witness.as_chunks::<33>(); @@ -884,6 +887,32 @@ mod tests { } } + #[test] + fn merkle_path_from_slice_rejects_truncated_input() { + const DEPTH: u8 = 4; + + // A valid encoding: the depth byte, DEPTH length-prefixed 32-byte + // nodes, and an 8-byte little-endian position. + let mut encoded = vec![DEPTH]; + for _ in 0..DEPTH { + encoded.push(32); + encoded.extend_from_slice(&[0u8; 32]); + } + encoded.extend_from_slice(&7u64.to_le_bytes()); + + assert_matches!(merkle_path_from_slice::(&encoded), Ok(_)); + + // Every strict prefix — including the empty slice, a missing node + // byte, and a truncated position — must return an error rather + // than panicking. + for len in 0..encoded.len() { + assert_matches!( + merkle_path_from_slice::(&encoded[..len]), + Err(_) + ); + } + } + proptest! { #[test] fn prop_commitment_tree_roundtrip_str(ct in arb_commitment_tree::<_, _, 8>(32, any::().prop_map(|c| c.to_string()))) { diff --git a/docs/changelog/unreleased/269.md b/docs/changelog/unreleased/269.md new file mode 100644 index 00000000..f047a766 --- /dev/null +++ b/docs/changelog/unreleased/269.md @@ -0,0 +1,34 @@ +## zakura-halo2-proofs + +### Fixed + +- Fixed `EvaluationDomain::l_i_range` to evaluate each Lagrange basis + polynomial to one at its own domain point instead of zero + ([#269](https://github.com/zakura-core/common/pull/269)). +- Made polynomial addition and `small_multiexp` panic on operands of + different lengths instead of silently mispairing them + ([#269](https://github.com/zakura-core/common/pull/269)). +- Made `poly::multiopen::create_proof` zero-extend query polynomials that + have fewer coefficients than the parameters' domain size, and reject ones + with more coefficients with an `InvalidInput` error instead of silently + truncating them ([#269](https://github.com/zakura-core/common/pull/269)). + +## zakura-pasta-curves + +### Fixed + +- Made the Pallas and Vesta `CurveExt::fft_vartime` implementations return + `false` for an `omega` whose multiplicative order is not exactly `2^log_n` + instead of returning an incorrect transform + ([#269](https://github.com/zakura-core/common/pull/269)). +- Documented that the closure returned by `CurveExt::hash_to_curve` panics + when the domain prefix exceeds 227 bytes for Pallas or 228 bytes for Vesta + ([#269](https://github.com/zakura-core/common/pull/269)). + +## zakura-primitives + +### Fixed + +- Fixed `merkle_path_from_slice` to return an error for an empty input + instead of panicking + ([#269](https://github.com/zakura-core/common/pull/269)).