# EPF Update 10&11 - In Math we Trust After understanding the entire flow of the Verkle Cryptography API, and how it's supposed to work in Nim, for the Nimbus client. I raised an [issue](https://github.com/mratsim/constantine/issues/275) soon after in [Constantine](https://github.com/mratsim/constantine/), and broke down my work into 15 objective tasks. They were soon after listed in my [PR](https://github.com/mratsim/constantine/pull/276). The tasks were as follows: Fixes #[275](https://github.com/mratsim/constantine/issues/275) - [X] Transcript generation using Fiat Shamir style - [X] Barycentric form using precompute optimisation over domain - [X] Common Reference String generation - [X] Common util funs for ipa - [X] IPA functions for prover - [X] IPA functions for verifier - [X] Functions for creating multiproofs - [X] Functions for checking multiproofs - [X] Random element generator for Pedersen - [X] Assert for no duplicate elements for random elem gen - [ ] Individual tests - [ ] Integrated tests - [X] Functions for Pedersen Commitments - [X] Adding more comments for explanation/ more resource links - [X] Refactoring to generics This to-do is updated to 7/10/2023. ## Barycentric form For the past few weeks I have been racking my head on how to implement the precompute approach for Lagrangian interpolation of the polynomials. However, soon after I figured out, thanks to this article. The followting is an excerpt from Kev's article. ### The Lagrange Polynomial We briefly restate the formula for a lagrange polynomial: $$ \mathcal{L_i}(X) = \prod_{j \neq i, j = 0}\frac{X -x_j}{x_i - x_j} $$ > The i'th lagrange polynomial evaluated at $x_i$ is 1 and 0 everywhere else **on the domain** ## First form of the barycentric interpolation formula We introduce the polynomial $A(X) = (X - x_0)(X - x_1)...(X-x_n)$. We also introduce the derivative of $A'(X) = \sum_{j=0}^{d-1}\prod_{i \neq j}(X - x_i)$ . > You can derive this yourself by generalising the product rule: <https://en.wikipedia.org/wiki/Product_rule#Product_of_more_than_two_factors> In general this derivative does not have a succint/sparse form. We do have a succint form if the domain is the roots of unity! Now note that $A'(x_j) = \prod_{i=0,i \neq j}(x_j - x_i)$ > If we plug in $x_k$ into $A'(X)$ all the terms with $X - x_k$ will vanish, this is why the sum disappears into a single product. We can use $A$ and $A'$ to re-define our lagrange polynomial as : $$ \mathcal{L_i}(X) = \frac{A(X)}{A'(x_i) (X - x_i)} $$ >Looking at the original lagrange formula, $A'(x_i)$ is the denominator and $\frac{A(X)}{X - x_i}$ is the numerator. The first barycentric form for a polynomial $f(X)$ can now be defined as : $$ f(X) = \sum_{i=0}^{d-1}{\frac{A(X)}{A'(x_i) (X - x_i)} f_i} $$ #### Remarks - $A(X)$ is not dependent on the values of $f_i$ and so can be brought out of the summation. - $A'(X)$ is only dependent on the domain, so it can be precomputed, along with $A(X)$ The Nim equivalent is stated [here](https://github.com/mratsim/constantine/blob/3292cd9a6c6b3e47da83d392fb29dc5c74e439ac/constantine/commitments/ipa/barycentric_form.nim). One of the main portions of the code is as follows: ```nim= func newPrecomputedWeights* [PrecomputedWeightsObj] (res: var PrecomputedWeights)= var midpoint: uint64 = DOMAIN var barycentricWeightsInst {.noInit.} : array[(midpoint*2), EC_P_Fr] for i in uint64(0)..midpoint: var weights {.noInit.}: EC_P_Fr weights.barycentricWeights(i) var inverseWeights {.noInit.}: FF inverseWeights.inv(weights) barycentricWeightsInst[i] = weights barycentricWeightsInst[i+midpoint] = inverseWeights midpoint = DOMAIN - 1 var invertedDomain: array[(midpoint*2), EC_P_Fr] for i in uint64(0)..DOMAIN: var k: EC_P_Fr = cast[EC_P_Fr](i) k.inv(k) var neg_k : EC_P_Fr var zero : FF zero.setZero() neg_k.diff(zero, k) invertedDomain[i-1] = k invertedDomain[(i-1) + midpoint] = neg_k res.barycentricWeights = barycentricWeightsInst res.invertedDomain = invertedDomain ``` Here the precomputed weights are $A(X)$ and $A'(X)$ as follows. The type defined to store them is as follows : ```nim= type PrecomputedWeights* = object #This stores A'(x_i) and 1/A'(x_i) barycentricWeights: openArray[EC_P_Fr] #This stores 1/k and -1/k for k in [0, 255] invertedDomain: openArray[EC_P_Fr] ``` The theoretical equivalent of this is here- For our case where the domain is the discrete interval $[0, 255]$ We only need to precompute $\frac{1}{x_i}$ for $x_i \in [-255, 255]$. This is 510 values, so we would store 510 * 32 = 16Kb. **How would we lookup these values?** First we imagine that we have stored the values in an array: $[\frac{1}{1}, \frac{1}{2}, \frac{1}{3}, \frac{1}{4}... \frac{1}{255},\frac{1}{-1},\frac{1}{-2},...\frac{1}{-255}]$ We first note that we can easily get from $\frac{1}{k}$ to $\frac{1}{-k}$ in the array by jumping forward 255 indices. Our strategy will be to find $\frac{1}{k}$ then jump to $\frac{1}{k}$ if we need to. **Example** We want to compute $\frac{1}{0 - 255}$. - Compute the $abs(0-255) = 255 = i$ In practice, we can use an if statement to check whether 255 or 0 is larger, and subtract accordingly. - Note that $\frac{1}{i}$ is at index $i-1$ - Since our original computation was $0 - 255$ which is negative, we need to get the element at index $i - 1 + 255$ ### Precomputing $\frac{A'(x_m)}{A'(x_i)}$ With the roots of unity, we did not need this optimisation as $\frac{A'(x_m)}{A'(x_i)}$ equaled $\frac{\omega^i}{\omega^m}$ which was trivial to fetch from the domain. For this, we will need to store precomputed values, if we want to efficiently compute $q_m$ in $O(d)$ steps, and to also avoid inversions. (Dankrad): We precompute $A'(x_i)$ and $\frac{1}{A'(x_i)}$. Given that we have 256 points in the domain. This will cost us 256 *2* 32 bytes = 16kB. **How would I lookup these values?** Similar to the previous optimisation, we store $A'(x_i)$ in an array. $[A'(0), A'(1), A'(2), A'(3)... A'(255),\frac{1}{A'(0)},\frac{1}{A'(1)},\frac{1}{A'(2)},...\frac{1}{A'(255)}]$ **Example** We want to compute $\frac{A'(0)}{A'(5)}$ - We can fetch $A'(0)$ by looking up the element at index $0$ in the array. - We can fetch $\frac{1}{A'(5)}$ by looking up the element at index 5, then jumping forward 256 positions. This is an optimisation that is being followed only in **Go** implementation apart from the Nim (Constantine) implementation, which is newly being developed. ## Other important Math functions for Transcript generation and Inner Product Arguments. Most of these functions exist in the `common_utils.nim` file in my Constantine PR. One of the very important tasks for me in here was writing a function to **Derive the generator element for Pedersen Commitment**. These are the set of dependencies ## Seed The current seed being used is: `eth_verkle_oct_2021` ## Rust implementation ```rust fn generate_random_elements(num_required_points: usize) -> Vec<EdwardsAffine> { use bandersnatch::Fq; use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(b"eth_verkle_oct_2021"); let bytes = hasher.finalize().to_vec(); let u = bandersnatch::Fq::from_be_bytes_mod_order(&bytes); let choose_largest = false; (0..) .into_iter() .map(|i| Fq::from(i as u128) + u) .map(|x| EdwardsAffine::get_point_from_x(x, choose_largest)) .filter_map(|point| point) .filter(|point| point.is_in_correct_subgroup_assuming_on_curve()) .take(num_required_points) .collect() } ``` For Nim we use the `Projective` coordinates, as that's what we've been using in order to make the library comparitively as fast as Go. Here is how the code looks like - ```nim= func generate_random_elements* [FF] (points: var openArray[FF], num_points: uint64) = var incrementer: uint64 = 0 while uint64(len(points)) != num_points: var digest : sha256 digest.init() digest.update(seed) var b {.noInit.} : array[8, byte] digest.update(b) var hash {.noInit.} : array[32, byte] digest.finish(hash) var x {.noInit.}: FF x.deserialize(hash) doAssert(cttCodecEcc_Success) ## Deserialisation success ! incrementer=incrementer+1 var x_as_Bytes {.noInit.} : array[32, byte] x_as_Bytes.serialize(x) doAssert(cttCodecEcc_Success) ## Serialisation Success ! var point_found {.noInit.} : EC_P point_found.deserialize(x_as_Bytes) doAssert(cttCodecEcc_Success) points[incrementer] = point_found ``` ## For the next article Sorry for writing articles biweekly, I just get lost into implementation sometimes, next update will be about how I'll be `implementing splitting and folding of scalars` while performing Pedersen Commitments inside the Inner Product Argument setting. I'll be also updating about how I'll be creating IPA proofs, creating a Multiproof using those IPA proofs and finally calling those proofs in a Multiproof Verifying Mechanism. Till then, best wishes :+1: