tests.rs
1 // Copyright (c) 2019-2025 Alpha-Delta Network Inc. 2 // This file is part of the deltavm library. 3 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at: 7 8 // http://www.apache.org/licenses/LICENSE-2.0 9 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 use crate::fft::{DensePolynomial, domain::*}; 17 use deltavm_curves::bls12_377::{Fr, G1Projective}; 18 use deltavm_fields::{FftField, Field, One, Zero}; 19 use deltavm_utilities::rand::{TestRng, Uniform}; 20 use rand::Rng; 21 22 #[test] 23 fn vanishing_polynomial_evaluation() { 24 let rng = &mut TestRng::default(); 25 26 for coeffs in 0..10 { 27 let domain = EvaluationDomain::<Fr>::new(coeffs).unwrap(); 28 let z = domain.vanishing_polynomial(); 29 for _ in 0..100 { 30 let point: Fr = rng.r#gen(); 31 assert_eq!(z.evaluate(point), domain.evaluate_vanishing_polynomial(point)) 32 } 33 } 34 } 35 36 #[test] 37 fn vanishing_polynomial_vanishes_on_domain() { 38 for coeffs in 0..1000 { 39 let domain = EvaluationDomain::<Fr>::new(coeffs).unwrap(); 40 let z = domain.vanishing_polynomial(); 41 for point in domain.elements() { 42 assert!(z.evaluate(point).is_zero()) 43 } 44 } 45 } 46 47 #[test] 48 fn size_of_elements() { 49 for coeffs in 1..10 { 50 let size = 1 << coeffs; 51 let domain = EvaluationDomain::<Fr>::new(size).unwrap(); 52 let domain_size = domain.size(); 53 assert_eq!(domain_size, domain.elements().count()); 54 } 55 } 56 57 #[test] 58 fn elements_contents() { 59 for coeffs in 1..10 { 60 let size = 1 << coeffs; 61 let domain = EvaluationDomain::<Fr>::new(size).unwrap(); 62 for (i, element) in domain.elements().enumerate() { 63 assert_eq!(element, domain.group_gen.pow([i as u64])); 64 } 65 } 66 } 67 68 /// Test that lagrange interpolation for a random polynomial at a random 69 /// point works. 70 #[test] 71 fn non_systematic_lagrange_coefficients_test() { 72 let mut rng = TestRng::default(); 73 74 for domain_dim in 1..10 { 75 let domain_size = 1 << domain_dim; 76 let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap(); 77 // Get random pt + lagrange coefficients 78 let rand_pt = Fr::rand(&mut rng); 79 let lagrange_coeffs = domain.evaluate_all_lagrange_coefficients(rand_pt); 80 81 // Sample the random polynomial, evaluate it over the domain and the random 82 // point. 83 let rand_poly = DensePolynomial::<Fr>::rand(domain_size - 1, &mut rng); 84 let poly_evals = domain.fft(rand_poly.coeffs()); 85 let actual_eval = rand_poly.evaluate(rand_pt); 86 87 // Do lagrange interpolation, and compare against the actual evaluation 88 let mut interpolated_eval = Fr::zero(); 89 for i in 0..domain_size { 90 interpolated_eval += lagrange_coeffs[i] * poly_evals[i]; 91 } 92 assert_eq!(actual_eval, interpolated_eval); 93 } 94 } 95 96 /// Test that lagrange coefficients for a point in the domain is correct 97 #[test] 98 fn systematic_lagrange_coefficients_test() { 99 // This runs in time O(N^2) in the domain size, so keep the domain dimension 100 // low. We generate lagrange coefficients for each element in the domain. 101 for domain_dim in 1..5 { 102 let domain_size = 1 << domain_dim; 103 let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap(); 104 let all_domain_elements: Vec<Fr> = domain.elements().collect(); 105 for (i, element) in all_domain_elements.iter().enumerate().take(domain_size) { 106 let lagrange_coeffs = domain.evaluate_all_lagrange_coefficients(*element); 107 for (j, &coeff) in lagrange_coeffs.iter().enumerate().take(domain_size) { 108 // Lagrange coefficient for the evaluation point, which should be 1 109 if i == j { 110 assert_eq!(coeff, Fr::one()); 111 } else { 112 assert_eq!(coeff, Fr::zero()); 113 } 114 } 115 } 116 } 117 } 118 119 #[test] 120 fn test_fft_correctness() { 121 // Tests that the ffts output the correct result. 122 // This assumes a correct polynomial evaluation at point procedure. 123 // It tests consistency of FFT/IFFT, and coset_fft/coset_ifft, 124 // along with testing that each individual evaluation is correct. 125 126 // Runs in time O(degree^2) 127 let log_degree = 5; 128 let degree = 1 << log_degree; 129 let rand_poly = DensePolynomial::<Fr>::rand(degree - 1, &mut TestRng::default()); 130 131 for log_domain_size in log_degree..(log_degree + 2) { 132 let domain_size = 1 << log_domain_size; 133 let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap(); 134 let poly_evals = domain.fft(&rand_poly.coeffs); 135 let poly_coset_evals = domain.coset_fft(&rand_poly.coeffs); 136 for (i, x) in domain.elements().enumerate() { 137 let coset_x = Fr::multiplicative_generator() * x; 138 139 assert_eq!(poly_evals[i], rand_poly.evaluate(x)); 140 assert_eq!(poly_coset_evals[i], rand_poly.evaluate(coset_x)); 141 } 142 143 let rand_poly_from_subgroup = DensePolynomial::from_coefficients_vec(domain.ifft(&poly_evals)); 144 let rand_poly_from_coset = DensePolynomial::from_coefficients_vec(domain.coset_ifft(&poly_coset_evals)); 145 146 assert_eq!(rand_poly, rand_poly_from_subgroup, "degree = {degree}, domain size = {domain_size}"); 147 assert_eq!(rand_poly, rand_poly_from_coset, "degree = {degree}, domain size = {domain_size}"); 148 } 149 } 150 151 #[test] 152 fn test_roots_of_unity() { 153 // Tests that the roots of unity result is the same as domain.elements() 154 let max_degree = 10; 155 for log_domain_size in 0..max_degree { 156 let domain_size = 1 << log_domain_size; 157 let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap(); 158 let actual_roots = domain.roots_of_unity(domain.group_gen); 159 for &value in &actual_roots { 160 assert!(domain.evaluate_vanishing_polynomial(value).is_zero()); 161 } 162 let expected_roots_elements = domain.elements(); 163 for (expected, &actual) in expected_roots_elements.zip(&actual_roots) { 164 assert_eq!(expected, actual); 165 } 166 assert_eq!(actual_roots.len(), domain_size / 2); 167 } 168 } 169 170 #[test] 171 #[cfg(not(feature = "serial"))] 172 fn parallel_fft_consistency() { 173 // This implements the Cooley-Turkey FFT, derived from libfqfft 174 // The libfqfft implementation uses pseudocode from [CLRS 2n Ed, pp. 864]. 175 fn serial_radix2_fft(a: &mut [Fr], omega: Fr, log_n: u32) { 176 #[inline] 177 pub(crate) fn bitreverse(mut n: u32, l: u32) -> u32 { 178 let mut r = 0; 179 for _ in 0..l { 180 r = (r << 1) | (n & 1); 181 n >>= 1; 182 } 183 r 184 } 185 use core::convert::TryFrom; 186 let n = u32::try_from(a.len()).expect("cannot perform FFTs larger on vectors of len > (1 << 32)"); 187 assert_eq!(n, 1 << log_n); 188 189 // swap coefficients in place 190 for k in 0..n { 191 let rk = bitreverse(k, log_n); 192 if k < rk { 193 a.swap(rk as usize, k as usize); 194 } 195 } 196 197 let mut m = 1; 198 for _i in 1..=log_n { 199 // w_m is 2^i-th root of unity 200 let w_m = omega.pow([(n / (2 * m)) as u64]); 201 202 let mut k = 0; 203 while k < n { 204 // w = w_m^j at the start of every loop iteration 205 let mut w = Fr::one(); 206 for j in 0..m { 207 let mut t = a[(k + j + m) as usize]; 208 t *= w; 209 let mut tmp = a[(k + j) as usize]; 210 tmp -= t; 211 a[(k + j + m) as usize] = tmp; 212 a[(k + j) as usize] += t; 213 w *= &w_m; 214 } 215 216 k += 2 * m; 217 } 218 219 m *= 2; 220 } 221 } 222 223 fn serial_radix2_ifft(a: &mut [Fr], omega: Fr, log_n: u32) { 224 serial_radix2_fft(a, omega.inverse().unwrap(), log_n); 225 let domain_size_inv = Fr::from(a.len() as u64).inverse().unwrap(); 226 for coeff in a.iter_mut() { 227 *coeff *= domain_size_inv; 228 } 229 } 230 231 fn serial_radix2_coset_fft(a: &mut [Fr], omega: Fr, log_n: u32) { 232 let coset_shift = Fr::multiplicative_generator(); 233 let mut cur_pow = Fr::one(); 234 for coeff in a.iter_mut() { 235 *coeff *= cur_pow; 236 cur_pow *= coset_shift; 237 } 238 serial_radix2_fft(a, omega, log_n); 239 } 240 241 fn serial_radix2_coset_ifft(a: &mut [Fr], omega: Fr, log_n: u32) { 242 serial_radix2_ifft(a, omega, log_n); 243 let coset_shift = Fr::multiplicative_generator().inverse().unwrap(); 244 let mut cur_pow = Fr::one(); 245 for coeff in a.iter_mut() { 246 *coeff *= cur_pow; 247 cur_pow *= coset_shift; 248 } 249 } 250 251 fn test_consistency<R: Rng>(rng: &mut R, max_coeffs: u32) { 252 for _ in 0..5 { 253 for log_d in 0..max_coeffs { 254 let d = 1 << log_d; 255 256 let expected_poly = (0..d).map(|_| Fr::rand(rng)).collect::<Vec<_>>(); 257 let mut expected_vec = expected_poly.clone(); 258 let mut actual_vec = expected_vec.clone(); 259 260 let domain = EvaluationDomain::new(d).unwrap(); 261 262 serial_radix2_fft(&mut expected_vec, domain.group_gen, log_d); 263 domain.fft_in_place(&mut actual_vec); 264 assert_eq!(expected_vec, actual_vec); 265 266 serial_radix2_ifft(&mut expected_vec, domain.group_gen, log_d); 267 domain.ifft_in_place(&mut actual_vec); 268 assert_eq!(expected_vec, actual_vec); 269 assert_eq!(expected_vec, expected_poly); 270 271 serial_radix2_coset_fft(&mut expected_vec, domain.group_gen, log_d); 272 domain.coset_fft_in_place(&mut actual_vec); 273 assert_eq!(expected_vec, actual_vec); 274 275 serial_radix2_coset_ifft(&mut expected_vec, domain.group_gen, log_d); 276 domain.coset_ifft_in_place(&mut actual_vec); 277 assert_eq!(expected_vec, actual_vec); 278 } 279 } 280 } 281 282 let rng = &mut TestRng::default(); 283 284 test_consistency(rng, 10); 285 } 286 287 #[test] 288 fn fft_composition() { 289 fn test_fft_composition<F: FftField, T: crate::fft::DomainCoeff<F> + Uniform + core::fmt::Debug + Eq, R: Rng>( 290 rng: &mut R, 291 max_coeffs: usize, 292 ) { 293 for coeffs in 0..max_coeffs { 294 let coeffs = 1 << coeffs; 295 296 let domain = EvaluationDomain::new(coeffs).unwrap(); 297 298 let mut v = vec![]; 299 for _ in 0..coeffs { 300 v.push(T::rand(rng)); 301 } 302 // Fill up with zeros. 303 v.resize(domain.size(), T::zero()); 304 let mut v2 = v.clone(); 305 306 domain.ifft_in_place(&mut v2); 307 domain.fft_in_place(&mut v2); 308 assert_eq!(v, v2, "ifft(fft(.)) != iden"); 309 310 domain.fft_in_place(&mut v2); 311 domain.ifft_in_place(&mut v2); 312 assert_eq!(v, v2, "fft(ifft(.)) != iden"); 313 314 domain.coset_ifft_in_place(&mut v2); 315 domain.coset_fft_in_place(&mut v2); 316 assert_eq!(v, v2, "coset_fft(coset_ifft(.)) != iden"); 317 318 domain.coset_fft_in_place(&mut v2); 319 domain.coset_ifft_in_place(&mut v2); 320 assert_eq!(v, v2, "coset_ifft(coset_fft(.)) != iden"); 321 } 322 } 323 324 let rng = &mut TestRng::default(); 325 326 test_fft_composition::<Fr, Fr, _>(rng, 10); 327 test_fft_composition::<Fr, G1Projective, _>(rng, 10); 328 } 329 330 #[test] 331 fn evaluate_over_domain() { 332 let rng = &mut TestRng::default(); 333 334 for domain_size in (1..10).map(|i| 2usize.pow(i)) { 335 let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap(); 336 for degree in [domain_size - 2, domain_size - 1, domain_size + 10] { 337 let p = DensePolynomial::rand(degree, rng); 338 assert_eq!( 339 p.evaluate_over_domain_by_ref(domain).evaluations, 340 domain.elements().map(|e| p.evaluate(e)).collect::<Vec<_>>() 341 ); 342 } 343 } 344 }