work on tensor, fix mul by constant(plaintext)
arnaucube git@arnaucube.com
Sun, 22 Jun 2025 18:42:50 +0200
4 files changed,
135 insertions(+),
196 deletions(-)
M
README.md
→
README.md
@@ -1,4 +1,5 @@
# fhe-study -Code done while studying some FHE papers, with the idea of doing implementations from scratch. +Implementations from scratch done while studying some FHE papers. -- arithmetic: contains $\mathbb{Z}_q$ and $\mathbb{Z}_q[X]/(X^N+1)$ arithmetic implementations, together with the NTT implementation. +- arithmetic: contains $\mathbb{Z}_q$, $R_q=\mathbb{Z}_q[X]/(X^N+1)$ and $R=\mathbb{Z}[X]/(X^N+1)$ arithmetic implementations, together with the NTT implementation. +- bfv: https://eprint.iacr.org/2012/144.pdf scheme implementation
M
arithmetic/src/ring.rs
→
arithmetic/src/ring.rs
@@ -59,6 +59,7 @@ .collect();
crate::Rq::<Q, N>::from_vec_f64(r) } } + pub fn mul_div_round<const Q: u64, const N: usize>( v: Vec<i64>, num: u64,@@ -73,8 +74,19 @@ // dbg!(&r);
crate::Rq::<Q, N>::from_vec_f64(r) } +// TODO rename to make it clear that is not mod q, but mod X^N+1 // apply mod (X^N+1) pub fn modulus<const N: usize>(p: &mut Vec<i64>) { + if p.len() < N { + return; + } + for i in N..p.len() { + p[i - N] = p[i - N].clone() - p[i].clone(); + p[i] = 0; + } + p.truncate(N); +} +pub fn modulus_i128<const N: usize>(p: &mut Vec<i128>) { if p.len() < N { return; }@@ -133,7 +145,7 @@ naive_poly_mul(self, rhs)
} } -// TODO with NTT(?) +// TODO WIP pub fn naive_poly_mul<const N: usize>(poly1: &R<N>, poly2: &R<N>) -> R<N> { let poly1: Vec<i128> = poly1.0.iter().map(|c| *c as i128).collect(); let poly2: Vec<i128> = poly2.0.iter().map(|c| *c as i128).collect();@@ -147,15 +159,35 @@
// apply mod (X^N + 1)) R::<N>::from_vec(result.iter().map(|c| *c as i64).collect()) } + pub fn naive_mul<const N: usize>(poly1: &R<N>, poly2: &R<N>) -> Vec<i64> { let poly1: Vec<i128> = poly1.0.iter().map(|c| *c as i128).collect(); let poly2: Vec<i128> = poly2.0.iter().map(|c| *c as i128).collect(); - let mut result = vec![0; (N * 2) - 1]; + let mut result: Vec<i128> = vec![0; (N * 2) - 1]; for i in 0..N { for j in 0..N { result[i + j] = result[i + j] + poly1[i] * poly2[j]; } } + + modulus_i128::<N>(&mut result); + // for c_i in result.iter() { + // println!("---"); + // println!("{:?}", &c_i); + // println!("{:?}", *c_i as i64); + // println!("{:?}", (*c_i as i64) as i128); + // assert_eq!(*c_i, (*c_i as i64) as i128, "{:?}", c_i); + // } + + // let q: i128 = 65537; + // let result: Vec<i64> = result + // .iter() + // // .map(|c_i| ((c_i % q + q) % q) as i64) + // .map(|c_i| (c_i % q) as i64) + // // .map(|c_i| *c_i as i64) + // .collect(); + // result + result.iter().map(|c| *c as i64).collect() }
M
arithmetic/src/ringq.rs
→
arithmetic/src/ringq.rs
@@ -56,6 +56,13 @@ pub fn to_r(self) -> crate::R<N> {
crate::R::<N>::from(self) } + pub fn zero() -> Self { + let coeffs = array::from_fn(|_| Zq::zero()); + Self { + coeffs, + evals: None, + } + } pub fn from_vec(coeffs: Vec<Zq<Q>>) -> Self { let mut p = coeffs; modulus::<Q, N>(&mut p);
M
bfv/src/lib.rs
→
bfv/src/lib.rs
@@ -10,12 +10,13 @@ use rand::Rng;
use rand_distr::{Normal, Uniform}; use std::ops; -use arithmetic::{Rq, Zq, R}; +use arithmetic::{Rq, R}; // error deviation for the Gaussian(Normal) distribution // sigma=3.2 from: https://eprint.iacr.org/2022/162.pdf page 5 const ERR_SIGMA: f64 = 3.2; // const ERR_SIGMA: f64 = 0.0; // TODO WIP +// const ERR_SIGMA: f64 = 1.0; // TODO WIP #[derive(Clone, Debug)] pub struct SecretKey<const Q: u64, const N: usize>(Rq<Q, N>);@@ -48,21 +49,23 @@ RLWE::<P, N>(x, y)
} fn tensor<const PQ: u64, const T: u64>(a: &Self, b: &Self) -> (Rq<Q, N>, Rq<Q, N>, Rq<Q, N>) { // expand Q->PQ // TODO rm + // get the coefficients in Z, ie. interpret a,b \in R (instead of R_q) let a0: R<N> = a.0.to_r(); let a1: R<N> = a.1.to_r(); let b0: R<N> = b.0.to_r(); let b1: R<N> = b.1.to_r(); - // tensor (\in R) + // tensor (\in R) (2021-204 p.9) use arithmetic::ring::naive_mul; + // (here can use *, but want to make it explicit that we're using the naive mul) let c0: Vec<i64> = naive_mul(&a0, &b0); let c1_l: Vec<i64> = naive_mul(&a0, &b1); let c1_r = naive_mul(&a1, &b0); let c1: Vec<i64> = itertools::zip_eq(c1_l, c1_r).map(|(l, r)| l + r).collect(); let c2: Vec<i64> = naive_mul(&a1, &b1); - // scale down, module Q, so result is \in R_q + // scale down, then reduce module Q, so result is \in R_q let c0: Rq<Q, N> = arithmetic::ring::mul_div_round::<Q, N>(c0, T, Q); let c1: Rq<Q, N> = arithmetic::ring::mul_div_round::<Q, N>(c1, T, Q); let c2: Rq<Q, N> = arithmetic::ring::mul_div_round::<Q, N>(c2, T, Q);@@ -119,76 +122,73 @@ fn add(self, rhs: &Rq<T, N>) -> Self::Output {
BFV::<Q, N, T>::add_const(self, rhs) } } -impl<const Q: u64, const N: usize, const T: u64> ops::Mul<&Rq<T, N>> for &RLWE<Q, N> { - type Output = RLWE<Q, N>; - fn mul(self, rhs: &Rq<T, N>) -> Self::Output { - BFV::<Q, N, T>::mul_const(&self, rhs) - } -} +// impl<const Q: u64, const N: usize, const T: u64> ops::Mul<&Rq<T, N>> for &RLWE<Q, N> { +// type Output = RLWE<Q, N>; +// fn mul(self, rhs: &Rq<T, N>) -> Self::Output { +// BFV::<Q, N, T>::mul_const(&self, rhs) +// } +// } pub struct BFV<const Q: u64, const N: usize, const T: u64> {} impl<const Q: u64, const N: usize, const T: u64> BFV<Q, N, T> { - const DELTA: u64 = Q / T; + const DELTA: u64 = Q / T; // floor /// generate a new key pair (privK, pubK) pub fn new_key(mut rng: impl Rng) -> Result<(SecretKey<Q, N>, PublicKey<Q, N>)> { // WIP: review probabilities - // let Xi_key = Uniform::new(-1_f64, 1_f64); - let Xi_key = Uniform::new(0_u64, 2_u64); + let Xi_key = Uniform::new(-1_f64, 1_f64); // let Xi_key = Uniform::new(0_u64, 2_u64); - // use rand::distributions::Bernoulli; - // let Xi_key = Bernoulli::new(0.5)?; let Xi_err = Normal::new(0_f64, ERR_SIGMA)?; + // let Xi_err = Normal::new(0_f64, 0.0)?; // secret key - let s = Rq::<Q, N>::rand_u64(&mut rng, Xi_key)?; + let s = Rq::<Q, N>::rand_f64(&mut rng, Xi_key)?; + // let s = Rq::<Q, N>::rand_u64(&mut rng, Xi_key)?; - #[cfg(test)] // sanity check - assert!(s.infinity_norm() <= 1, "{:?}", s.coeffs()); + // #[cfg(test)] // sanity check + // assert!( + // s.infinity_norm() <= 1, + // "s.infinity_norm check failed {:?}", + // s.coeffs() + // ); // pk = (-a * s + e, a) let a = Rq::<Q, N>::rand_u64(&mut rng, Uniform::new(0_u64, Q))?; let e = Rq::<Q, N>::rand_f64(&mut rng, Xi_err)?; + // println!("e{:?}", e.coeffs()); let pk: PublicKey<Q, N> = PublicKey((&(-a) * &s) + e, a.clone()); Ok((SecretKey(s), pk)) } pub fn encrypt(mut rng: impl Rng, pk: &PublicKey<Q, N>, m: &Rq<T, N>) -> Result<RLWE<Q, N>> { - // let Xi_key = Uniform::new(-1_f64, 1_f64); - // let Xi_key = Uniform::new(0_f64, 2_f64); - let Xi_key = Uniform::new(0_u64, 2_u64); + let Xi_key = Uniform::new(-1_f64, 1_f64); + // let Xi_key = Uniform::new(0_u64, 2_u64); let Xi_err = Normal::new(0_f64, ERR_SIGMA)?; + // let Xi_err = Normal::new(0_f64, 0.0)?; - let u = Rq::<Q, N>::rand_u64(&mut rng, Xi_key)?; - let e_1 = Rq::<Q, N>::rand_f64_abs(&mut rng, Xi_err)?; - let e_2 = Rq::<Q, N>::rand_f64_abs(&mut rng, Xi_err)?; + let u = Rq::<Q, N>::rand_f64(&mut rng, Xi_key)?; + // let u = Rq::<Q, N>::rand_u64(&mut rng, Xi_key)?; + let e_1 = Rq::<Q, N>::rand_f64(&mut rng, Xi_err)?; + let e_2 = Rq::<Q, N>::rand_f64(&mut rng, Xi_err)?; + // println!("e_1{:?}", e_1.coeffs()); + // println!("e_2{:?}", e_2.coeffs()); - // println!("{:?}", &e_1.coeffs()); - // println!("{:?}", &e_2.coeffs()); - - #[cfg(test)] // sanity check - assert!(u.infinity_norm() <= 1, "{:?}", u.coeffs()); + // #[cfg(test)] // sanity check + // assert!( + // u.infinity_norm() <= 1, + // "u.infinity_norm check failed {:?}", + // u.coeffs() + // ); // migrate m's coeffs to the bigger modulus Q (from T) let m = m.remodule::<Q>(); - #[cfg(test)] - { - // sanity check // TODO rm - let m_remod_naive = - Rq::<Q, N>::from_vec_u64(m.coeffs().iter().map(|m_i| m_i.0).collect()); - assert_eq!(m_remod_naive, m); - } - - // let c0 = &pk.0 * &u + e_1 + m * Self::DELTA; - let c0 = &pk.0 * &u + e_1 + m.mul_div_round(Q, T); // TODO use DELTA? + // let c0 = &pk.0 * &u + e_1 + m.mul_div_round(Q, T); + let c0 = &pk.0 * &u + e_1 + m * Self::DELTA; + // let D: u64 = (Q as f64 / T as f64).floor() as u64; + // let c0 = &pk.0 * &u + e_1 + m * D; // TODO use DELTA? let c1 = &pk.1 * &u + e_2; - // let c0 = tmp_naive_mul(pk.0, u) + e_1 + m * Self::DELTA; - // let c0 = tmp_naive_mul(pk.0, u) + e_1 + m.mul_div_round(Q, T); - // let c1 = tmp_naive_mul(pk.1, u) - // // &pk.1 * &u - // + e_2; Ok(RLWE::<Q, N>(c0, c1)) }@@ -216,10 +216,13 @@ // assuming T<Q, move m from Zq<T> to Zq<Q>
let m = m.remodule::<Q>(); RLWE::<Q, N>(c.0 + m * Self::DELTA, c.1) } - fn mul_const(c: &RLWE<Q, N>, m: &Rq<T, N>) -> RLWE<Q, N> { + fn mul_const<const PQ: u64>(rlk: &RLK<PQ, N>, c: &RLWE<Q, N>, m: &Rq<T, N>) -> RLWE<Q, N> { // assuming T<Q, move m from Zq<T> to Zq<Q> let m = m.remodule::<Q>(); - RLWE::<Q, N>(c.0 * m * Self::DELTA, c.1) + + // encrypt m*Delta without noise, and then perform normal ciphertext multiplication + let md = RLWE::<Q, N>(m * Self::DELTA, Rq::zero()); + RLWE::<Q, N>::mul::<PQ, T>(&rlk, &c, &md) } fn rlk_key<const PQ: u64>(mut rng: impl Rng, s: &SecretKey<Q, N>) -> Result<RLK<PQ, N>> {@@ -232,11 +235,11 @@
// let rlk_1: Rq<PQ, N> = (&(-a) * &s) - e + (s * s) * P; let P = PQ / Q; - // let rlk: RLK<PQ, N> = RLK::<PQ, N>((&(-a) * &s) - e + (s * s) * P, a.clone()); - let rlk: RLK<PQ, N> = RLK::<PQ, N>( - -(tmp_naive_mul(a, s) + e) + tmp_naive_mul(s, s) * P, - a.clone(), - ); + let rlk: RLK<PQ, N> = RLK::<PQ, N>((&(-a) * &s) - e + (s * s) * P, a.clone()); + // let rlk: RLK<PQ, N> = RLK::<PQ, N>( + // -(tmp_naive_mul(a, s) + e) + tmp_naive_mul(s, s) * P, + // a.clone(), + // ); // let rlk: RLK<PQ, N> = RLK::<PQ, N>(-(&a * &s + e) + (s * s) * P, a.clone()); // let pq = P * Q;@@ -292,17 +295,8 @@
// let c2rlk0: Vec<f64> = (c2.remodule::<PQ>() * rlk.0) use arithmetic::ring::naive_mul; let c2rlk0: Vec<i64> = naive_mul(&c2.to_r(), &rlk.0.to_r()); - // .coeffs() - // .iter() - // .map(|e| (*e as f64 / P as f64).round()) - // .collect(); + let c2rlk1: Vec<i64> = naive_mul(&c2.to_r(), &rlk.1.to_r()); - let c2rlk1: Vec<i64> = naive_mul(&c2.to_r(), &rlk.1.to_r()); - // .coeffs() - // .iter() - // .map(|e| (*e as f64 / P as f64).round()) - // .collect(); - // let r0: Rq<Q, N> = arithmetic::ring::mul_div_round::<Q, N>(c2rlk0, 1, P); let r1: Rq<Q, N> = arithmetic::ring::mul_div_round::<Q, N>(c2rlk1, 1, P);@@ -348,36 +342,37 @@
#[test] fn test_addition() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; - // const N: usize = 32; - const N: usize = 4; + const N: usize = 32; const T: u64 = 4; // plaintext modulus type S = BFV<Q, N, T>; let mut rng = rand::thread_rng(); - let (sk, pk) = S::new_key(&mut rng)?; + for _ in 0..1_000 { + let (sk, pk) = S::new_key(&mut rng)?; - let msg_dist = Uniform::new(0_u64, T); - let m1 = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; - let m2 = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; + let msg_dist = Uniform::new(0_u64, T); + let m1 = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; + let m2 = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; - let c1 = S::encrypt(&mut rng, &pk, &m1)?; - let c2 = S::encrypt(&mut rng, &pk, &m2)?; + let c1 = S::encrypt(&mut rng, &pk, &m1)?; + let c2 = S::encrypt(&mut rng, &pk, &m2)?; - let c3 = c1 + c2; + let c3 = c1 + c2; - let m3_recovered = S::decrypt(&sk, &c3); + let m3_recovered = S::decrypt(&sk, &c3); - assert_eq!(m1 + m2, m3_recovered); + assert_eq!(m1 + m2, m3_recovered); + } Ok(()) } #[test] - fn test_constant_add() -> Result<()> { + fn test_constant_add_mul() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; const N: usize = 32; - const T: u64 = 4; // plaintext modulus + const T: u64 = 8; // plaintext modulus type S = BFV<Q, N, T>; let mut rng = rand::thread_rng();@@ -387,25 +382,25 @@
let msg_dist = Uniform::new(0_u64, T); let m1 = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; let m2_const = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; - let c1 = S::encrypt(&mut rng, &pk, &m1)?; let c3_add = &c1 + &m2_const; - // let c3_mul = &c1 * &m2_const; let m3_add_recovered = S::decrypt(&sk, &c3_add); - // let m3_mul_recovered = S::decrypt(&sk, &c3_mul); - assert_eq!(m1 + m2_const, m3_add_recovered); - // - // let mut mul_res = naive_poly_mul::<T>(&m1.coeffs().to_vec(), &m2_const.coeffs().to_vec()); - // arithmetic::ring::modulus::<T, N>(&mut mul_res); - // dbg!(&mul_res); - // let mul_res_2 = - // naive_poly_mul_2::<T, N>(&m1.coeffs().to_vec(), &m2_const.coeffs().to_vec()); - // assert_eq!(mul_res, mul_res_2); - // let mul_res = PR::<T, N>::from_vec(mul_res); - // assert_eq!(mul_res.coeffs(), m3_mul_recovered.coeffs()); + + // test multiplication of a ciphertext by a constant + const P: u64 = Q * Q; + const PQ: u64 = P * Q; + let rlk = BFV::<Q, N, T>::rlk_key::<PQ>(&mut rng, &sk)?; + + let c3_mul = S::mul_const(&rlk, &c1, &m2_const); + + let m3_mul_recovered = S::decrypt(&sk, &c3_mul); + assert_eq!( + (m1.to_r() * m2_const.to_r()).to_rq::<T>().coeffs(), + m3_mul_recovered.coeffs() + ); Ok(()) }@@ -413,12 +408,11 @@
#[test] fn test_tensor() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; // q prime, and 2^q + 1 shape - const N: usize = 8; - const T: u64 = 4; // plaintext modulus + const N: usize = 32; + const T: u64 = 8; // plaintext modulus // const P: u64 = Q; const P: u64 = Q * Q; - // const P: u64 = 2_u64.pow(13) * Q + 1; const PQ: u64 = P * Q; let mut rng = rand::thread_rng();@@ -447,7 +441,13 @@ let (c_a, c_b, c_c) = RLWE::<Q, N>::tensor::<PQ, T>(&c1, &c2);
// decrypt non-relinearized mul result let m3: Rq<Q, N> = c_a + c_b * sk.0 + c_c * sk.0 * sk.0; - let m3: Rq<Q, N> = m3.mul_div_round(T, Q); + // let m3: Rq<Q, N> = c_a + // + Rq::<Q, N>::from_vec_i64(arithmetic::ring::naive_mul(&c_b.to_r(), &sk.0.to_r())) + // + Rq::<Q, N>::from_vec_i64(arithmetic::ring::naive_mul( + // &c_c.to_r(), + // &R::<N>::from_vec(arithmetic::ring::naive_mul(&sk.0.to_r(), &sk.0.to_r())), + // )); + let m3: Rq<Q, N> = m3.mul_div_round(T, Q); // descale let m3 = m3.remodule::<T>(); let naive = (m1.to_r() * m2.to_r()).to_rq::<T>();@@ -538,7 +538,7 @@
let c1 = BFV::<Q, N, T>::encrypt(&mut rng, &pk, &m1)?; let c2 = BFV::<Q, N, T>::encrypt(&mut rng, &pk, &m2)?; - let c3 = RLWE::<Q, N>::mul::<PQ, T>(&rlk, &c1, &c2); + let c3 = RLWE::<Q, N>::mul::<PQ, T>(&rlk, &c1, &c2); // uses relinearize internally let m3 = BFV::<Q, N, T>::decrypt(&sk, &c3);@@ -548,107 +548,6 @@ "\n\nfor testing:\nlet m1 = Rq::<T, N>::from_vec_u64(vec!{:?});\nlet m2 = Rq::<T, N>::from_vec_u64(vec!{:?});\n",
m1.coeffs(), m2.coeffs() ); - - Ok(()) - } - - #[test] - fn test_naive_mul() -> Result<()> { - const Q: u64 = 2u64.pow(16) + 1; // prime, and 2^q + 1 shape - const N: usize = 4; - const T: u64 = 4; // plaintext modulus - - let mut rng = rand::thread_rng(); - let msg_dist = Uniform::new(0_u64, T); - // for _ in 0..10_000 { - for _ in 0..2 { - println!("---"); - // let a = Rq::<Q, N>::rand_u64(&mut rng, msg_dist)?; - // let b = Rq::<Q, N>::rand_u64(&mut rng, msg_dist)?; - // let a = Rq::<Q, N>::from_vec_u64(vec![Q - 1, Q - 2, Q - 3, Q - 3]); - // let b = Rq::<Q, N>::from_vec_u64(vec![Q - 3, Q - 3, Q - 2, Q - 1]); - let a = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; - let b = Rq::<T, N>::rand_u64(&mut rng, msg_dist)?; - dbg!(&a); - dbg!(&b); - let (_, pk) = BFV::<Q, N, T>::new_key(&mut rng)?; - let ciph1 = BFV::<Q, N, T>::encrypt(&mut rng, &pk, &a)?; - let ciph2 = BFV::<Q, N, T>::encrypt(&mut rng, &pk, &b)?; - let a = ciph1.0; - let b = ciph2.0; - dbg!(&a); - dbg!(&b); - - let c0: Vec<i64> = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); - let c0 = Rq::<Q, N>::from_vec_i64(c0); - let c1 = tmp_naive_mul(a, b); // naive mul - let c2: Rq<Q, N> = a * b; // NTT mul - - println!("{:?}", c0.coeffs()); - println!("{:?}", c1.coeffs()); - println!("{:?}", c2.coeffs()); - assert_eq!(c0, c2); - assert_eq!(c1, c2); - - // scale by Delta=Q/T - let a = a.mul_div_round(Q, T); - let b = b.mul_div_round(Q, T); - dbg!(&a); - dbg!(&b); - - let c0: Vec<i64> = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); - let c0 = Rq::<Q, N>::from_vec_i64(c0); - let c1 = tmp_naive_mul(a, b); // naive mul - let c2: Rq<Q, N> = a * b; // NTT mul - - println!("{:?}", c0.coeffs()); - println!("{:?}", c1.coeffs()); - println!("{:?}", c2.coeffs()); - assert_eq!(c0, c2); - assert_eq!(c1, c2); - - let c0 = c0.mul_div_round(T, Q); - let c1 = c1.mul_div_round(T, Q); - let c2 = c2.mul_div_round(T, Q); - println!("{:?}", c0.coeffs()); - println!("{:?}", c1.coeffs()); - println!("{:?}", c2.coeffs()); - assert_eq!(c0, c2); - assert_eq!(c1, c2); - - /* - // now same as before, but multiplying by T/Q - let c0: Vec<i64> = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); - let c0: Vec<f64> = c0 - .iter() - .map(|e| ((T as f64 * *e as f64) / Q as f64).round()) - .collect(); - let c0 = Rq::<Q, N>::from_vec_f64(c0); - dbg!(&c0.coeffs()); - - let a = a.mul_div_round(T, Q); - let b = b.mul_div_round(T, Q); - println!("a{:?}", a.coeffs()); - println!("b{:?}", b.coeffs()); - - let c4: Vec<i64> = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); - let c4 = Rq::<Q, N>::from_vec_i64(c4); - let c4 = c4.mul_div_round(T, Q); - - let c1 = tmp_naive_mul(a, b); // naive mul - let c1 = c1.mul_div_round(T, Q); - let c2 = a * b; // NTT mul - let c2 = c2.mul_div_round(T, Q); - - println!("{:?}", c0.coeffs()); - println!("{:?}", c1.coeffs()); - println!("{:?}", c2.coeffs()); - println!("{:?}", c4.coeffs()); - assert_eq!(c0, c2); - assert_eq!(c4, c2); - assert_eq!(c1, c2); - */ - } Ok(()) }