├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── README.md ├── rust-toolchain └── src ├── circulant.rs ├── lib.rs └── toeplitz.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI checks 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, opened, reopened, ready_for_review] 6 | push: 7 | branches: 8 | - master 9 | 10 | env: 11 | RUSTFLAGS: "-D warnings" 12 | 13 | ## `actions-rs/toolchain@v1` overwrite set to false so that 14 | ## `rust-toolchain` is always used and the only source of truth. 15 | 16 | jobs: 17 | test: 18 | if: github.event.pull_request.draft == false 19 | 20 | name: Test on ${{ matrix.os }} 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | # We don't need to test across multiple platforms yet 25 | # os: [ubuntu-latest, windows-latest, macOS-latest] 26 | os: [ubuntu-latest] 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: webfactory/ssh-agent@v0.7.0 31 | with: 32 | ssh-private-key: ${{ secrets.FAST_EVAL_SSH_KEY }} 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | override: false 36 | - name: Run tests 37 | uses: actions-rs/cargo@v1 38 | with: 39 | command: test 40 | args: --verbose --release --workspace --all-features 41 | # - name: Check Benchmarks 42 | # uses: actions-rs/cargo@v1 43 | # with: 44 | # command: bench 45 | # args: --verbose --workspace --all-features --no-run 46 | 47 | build: 48 | if: github.event.pull_request.draft == false 49 | 50 | name: Build target ${{ matrix.target }} 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | target: 55 | - wasm32-unknown-unknown 56 | - wasm32-wasi 57 | 58 | steps: 59 | - uses: actions/checkout@v2 60 | - uses: webfactory/ssh-agent@v0.7.0 61 | with: 62 | ssh-private-key: ${{ secrets.FAST_EVAL_SSH_KEY }} 63 | - uses: actions-rs/toolchain@v1 64 | with: 65 | override: false 66 | - name: Add target 67 | run: rustup target add ${{ matrix.target }} 68 | - name: cargo build 69 | uses: actions-rs/cargo@v1 70 | with: 71 | command: build 72 | args: --all-features 73 | 74 | doc-links: 75 | if: github.event.pull_request.draft == false 76 | 77 | name: Intra-doc links 78 | runs-on: ubuntu-latest 79 | 80 | steps: 81 | - uses: actions/checkout@v2 82 | - uses: webfactory/ssh-agent@v0.7.0 83 | with: 84 | ssh-private-key: ${{ secrets.FAST_EVAL_SSH_KEY }} 85 | - uses: actions-rs/toolchain@v1 86 | with: 87 | override: false 88 | - name: cargo fetch 89 | uses: actions-rs/cargo@v1 90 | with: 91 | command: fetch 92 | 93 | # Ensure intra-documentation links all resolve correctly 94 | # Requires #![deny(intra_doc_link_resolution_failure)] in crates. 95 | - name: Check intra-doc links 96 | uses: actions-rs/cargo@v1 97 | with: 98 | command: doc 99 | args: --workspace --all-features 100 | 101 | fmt: 102 | if: github.event.pull_request.draft == false 103 | 104 | name: Rustfmt 105 | timeout-minutes: 30 106 | runs-on: ubuntu-latest 107 | steps: 108 | - uses: actions/checkout@v2 109 | - uses: webfactory/ssh-agent@v0.7.0 110 | with: 111 | ssh-private-key: ${{ secrets.FAST_EVAL_SSH_KEY }} 112 | - uses: actions-rs/toolchain@v1 113 | with: 114 | override: false 115 | - run: rustup component add rustfmt 116 | - uses: actions-rs/cargo@v1 117 | with: 118 | command: fmt 119 | args: --all -- --check 120 | 121 | clippy: 122 | if: github.event.pull_request.draft == false 123 | 124 | name: Clippy lint checks 125 | runs-on: ubuntu-latest 126 | 127 | steps: 128 | - uses: actions/checkout@v2 129 | - uses: webfactory/ssh-agent@v0.7.0 130 | with: 131 | ssh-private-key: ${{ secrets.FAST_EVAL_SSH_KEY }} 132 | - uses: actions-rs/toolchain@v1 133 | with: 134 | override: false 135 | components: clippy 136 | - name: Run clippy 137 | uses: actions-rs/cargo@v1 138 | with: 139 | command: clippy 140 | args: --verbose --release --tests --all-features 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "toeplitz" 4 | ] 5 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fk" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ark-ff = "0.3.0" 10 | ndarray = "0.15.6" 11 | ark-poly = "0.3.0" 12 | ark-ec = "0.3.0" 13 | ark-std = "0.3.0" 14 | 15 | [dev-dependencies] 16 | ark-bn254 = "0.3.0" 17 | ndarray = "0.15.6" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fk 2 | Fast multiplication of Toeplitz matrix by a vector, based on https://eprint.iacr.org/2020/1516.pdf 3 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.66.0 -------------------------------------------------------------------------------- /src/circulant.rs: -------------------------------------------------------------------------------- 1 | use ark_ff::FftField; 2 | use ark_poly::{domain::DomainCoeff, EvaluationDomain, GeneralEvaluationDomain}; 3 | use std::{fmt::Debug, marker::PhantomData}; 4 | 5 | use crate::is_pow_2; 6 | 7 | pub struct Circulant + Debug> { 8 | _f: PhantomData, 9 | _d: PhantomData, 10 | } 11 | 12 | impl + Debug> Circulant { 13 | pub fn mul_by_vec + std::ops::MulAssign>(repr: &[D], x: &[T]) -> Vec { 14 | assert!(is_pow_2(repr.len())); 15 | let domain = GeneralEvaluationDomain::new(repr.len()).unwrap(); 16 | let v = domain.fft(repr); 17 | 18 | let mut res = domain.fft(x); 19 | for (i, _) in x.iter().enumerate() { 20 | res[i] *= v[i] 21 | } 22 | 23 | domain.ifft(&res) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use ark_std::log2; 2 | 3 | mod circulant; 4 | mod toeplitz; 5 | 6 | pub fn is_pow_2(x: usize) -> bool { 7 | (x & (x - 1)) == 0 8 | } 9 | 10 | pub fn next_pow2(n: usize) -> usize { 11 | let two: u32 = 2; 12 | let a: u32 = log2(n); 13 | 14 | if two.pow(a - 1) == n as u32 { 15 | return n; 16 | } 17 | 18 | two.pow(a).try_into().unwrap() 19 | } 20 | 21 | pub use toeplitz::UpperToeplitz; 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use std::iter; 26 | 27 | use ark_bn254::{Bn254, Fr, G1Affine, G1Projective}; 28 | use ark_ec::{msm::VariableBaseMSM, AffineCurve, PairingEngine}; 29 | use ark_ff::{One, PrimeField, UniformRand, Zero}; 30 | use ark_poly::{ 31 | univariate::DensePolynomial, EvaluationDomain, GeneralEvaluationDomain, Polynomial, 32 | UVPolynomial, 33 | }; 34 | use ark_std::test_rng; 35 | 36 | use crate::{next_pow2, toeplitz::UpperToeplitz}; 37 | 38 | pub fn commit( 39 | srs: &[E::G1Affine], 40 | poly: &DensePolynomial, 41 | ) -> E::G1Projective { 42 | if srs.len() - 1 < poly.degree() { 43 | panic!( 44 | "SRS size to small! Can't commit to polynomial of degree {} with srs of size {}", 45 | poly.degree(), 46 | srs.len() 47 | ); 48 | } 49 | let coeff_scalars: Vec<_> = poly.coeffs.iter().map(|c| c.into_repr()).collect(); 50 | VariableBaseMSM::multi_scalar_mul(srs, &coeff_scalars) 51 | } 52 | 53 | pub fn open( 54 | srs: &[E::G1Affine], 55 | poly: &DensePolynomial, 56 | challenge: E::Fr, 57 | ) -> (E::Fr, E::G1Affine) { 58 | let q = poly / &DensePolynomial::from_coefficients_slice(&[-challenge, E::Fr::one()]); 59 | if srs.len() - 1 < q.degree() { 60 | panic!( 61 | "Open g1: SRS size to small! Can't commit to polynomial of degree {} with srs of size {}", 62 | q.degree(), 63 | srs.len() 64 | ); 65 | } 66 | let proof: E::G1Affine = commit::(srs, &q).into(); 67 | (poly.evaluate(&challenge), proof) 68 | } 69 | 70 | fn commit_in_each_omega_i( 71 | srs: &[E::G1Affine], 72 | domain: &GeneralEvaluationDomain, 73 | poly: &DensePolynomial, 74 | ) -> Vec { 75 | domain 76 | .elements() 77 | .map(|omega_pow_i| open::(srs, poly, omega_pow_i).1) 78 | .collect() 79 | } 80 | 81 | #[test] 82 | fn test_multipoint_commitment() { 83 | let n = 64; 84 | let mut rng = test_rng(); 85 | 86 | let tau = Fr::rand(&mut rng); 87 | 88 | let powers_of_tau: Vec = iter::successors(Some(Fr::one()), |p| Some(*p * tau)) 89 | .take(n) 90 | .collect(); 91 | 92 | let g1_gen = G1Affine::prime_subgroup_generator(); 93 | 94 | let srs: Vec = powers_of_tau 95 | .iter() 96 | .take(n) 97 | .map(|tp| g1_gen.mul(tp.into_repr()).into()) 98 | .collect(); 99 | 100 | let mut srs_proj: Vec = srs.iter().map(|t| t.into_projective()).collect(); 101 | srs_proj.reverse(); 102 | 103 | let poly = DensePolynomial::::rand(n, &mut rng); 104 | let t = UpperToeplitz::from_poly(&poly); 105 | 106 | let h_commitments = t.mul_by_vec(&srs_proj)[..n].to_vec(); 107 | 108 | let domain = GeneralEvaluationDomain::::new(n).unwrap(); 109 | 110 | let qs_fast = domain.fft(&h_commitments); 111 | let qs_slow = commit_in_each_omega_i::(&srs, &domain, &poly); 112 | assert_eq!(qs_fast, qs_slow); 113 | } 114 | 115 | #[test] 116 | fn test_smaller_degree() { 117 | let n = 32; 118 | let domain = GeneralEvaluationDomain::::new(n).unwrap(); 119 | let mut rng = test_rng(); 120 | 121 | let tau = Fr::rand(&mut rng); 122 | 123 | let powers_of_tau: Vec = iter::successors(Some(Fr::one()), |p| Some(*p * tau)) 124 | .take(n) 125 | .collect(); 126 | 127 | let g1_gen = G1Affine::prime_subgroup_generator(); 128 | 129 | let srs: Vec = powers_of_tau 130 | .iter() 131 | .take(n) 132 | .map(|tp| g1_gen.mul(tp.into_repr()).into()) 133 | .collect(); 134 | 135 | let d = 5; 136 | let next_pow_2_deg = next_pow2(d); 137 | let mut srs_proj: Vec = srs 138 | .iter() 139 | .take(next_pow_2_deg) 140 | .map(|t| t.into_projective()) 141 | .collect(); 142 | srs_proj.reverse(); 143 | 144 | let poly = DensePolynomial::::rand(d, &mut rng); 145 | 146 | let t = UpperToeplitz::from_poly(&poly); 147 | 148 | let mut h_commitments = t.mul_by_vec(&srs_proj)[..next_pow_2_deg].to_vec(); 149 | let zero_cms = vec![G1Projective::zero(); n - next_pow_2_deg]; 150 | h_commitments.extend_from_slice(&zero_cms); 151 | 152 | let qs_fast = domain.fft(&h_commitments); 153 | let qs_slow = commit_in_each_omega_i::(&srs, &domain, &poly); 154 | assert_eq!(qs_fast, qs_slow); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/toeplitz.rs: -------------------------------------------------------------------------------- 1 | use ark_ff::{FftField, Zero}; 2 | use ark_poly::{domain::DomainCoeff, univariate::DensePolynomial, Polynomial, UVPolynomial}; 3 | 4 | use crate::{circulant::Circulant, is_pow_2, next_pow2}; 5 | 6 | /* 7 | fm f(m-1) ... f1 8 | 0 fm ... f2 9 | . ... ... f3 10 | . ... fm f(m-1) 11 | 0 ... ... fm 12 | */ 13 | /// Succinct representation of Toeplitz matrix that is instantiated from polynomial 14 | /// on which mul by vector can be run efficiently 15 | pub struct UpperToeplitz { 16 | pub(crate) repr: Vec, 17 | } 18 | 19 | impl UpperToeplitz { 20 | pub fn from_poly(poly: &DensePolynomial) -> Self { 21 | let mut repr = poly.coeffs()[1..].to_vec(); 22 | let next_pow2_degree = next_pow2(poly.degree()); 23 | let to_extend = vec![F::zero(); next_pow2_degree - poly.degree()]; 24 | repr.extend_from_slice(&to_extend); 25 | assert!(is_pow_2(repr.len())); 26 | Self { repr } 27 | } 28 | 29 | pub fn mul_by_vec + std::ops::MulAssign + Zero>(&self, x: &[T]) -> Vec { 30 | let circulant_repr = self.to_circulant_repr(); 31 | let zeroes = vec![T::zero(); x.len()]; 32 | Circulant::mul_by_vec(&circulant_repr, &[x, zeroes.as_slice()].concat()) 33 | } 34 | 35 | fn to_circulant_repr(&self) -> Vec { 36 | let fm = *self.repr.last().unwrap(); 37 | let mut circulant_repr = vec![F::zero(); self.repr.len() + 1]; 38 | 39 | circulant_repr[0] = fm; 40 | circulant_repr[self.repr.len()] = fm; 41 | 42 | circulant_repr.extend_from_slice(&self.repr[..self.repr.len() - 1]); 43 | circulant_repr 44 | } 45 | } 46 | --------------------------------------------------------------------------------