<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Adam</title>
    <description>The latest articles on Forem by Adam (@rustarians).</description>
    <link>https://forem.com/rustarians</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810250%2F507cea5d-d6ec-4228-8c7a-265d5ec255f6.png</url>
      <title>Forem: Adam</title>
      <link>https://forem.com/rustarians</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rustarians"/>
    <language>en</language>
    <item>
      <title>10 Questions from a ZK Meetup in Krakow</title>
      <dc:creator>Adam</dc:creator>
      <pubDate>Tue, 10 Mar 2026 11:22:52 +0000</pubDate>
      <link>https://forem.com/rustarians/10-questions-from-a-zk-meetup-in-krakow-39c</link>
      <guid>https://forem.com/rustarians/10-questions-from-a-zk-meetup-in-krakow-39c</guid>
      <description>&lt;p&gt;&lt;em&gt;Original version with full formatting on &lt;a href="https://rustarians.com/10-questions-from-a-zk-meetup-in-krakow/" rel="noopener noreferrer"&gt;rustarians.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I gave a talk about halo2 at a Rust meetup in Krakow. The audience asked 10 questions I didn't have time to answer properly on stage, so I wrote them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Where would I actually use this?&lt;/li&gt;
&lt;li&gt;How does an execution trace work if you don't know the next row?&lt;/li&gt;
&lt;li&gt;Where do I start learning?&lt;/li&gt;
&lt;li&gt;Why only addition and multiplication?&lt;/li&gt;
&lt;li&gt;How would mObywatel work with ZK?&lt;/li&gt;
&lt;li&gt;Can I use bounds larger than 8 bits?&lt;/li&gt;
&lt;li&gt;Can you formally prove a circuit is correct, e.g. with Lean?&lt;/li&gt;
&lt;li&gt;Is this a SNARK or a STARK?&lt;/li&gt;
&lt;li&gt;ZK in electronic voting&lt;/li&gt;
&lt;li&gt;Is ZK verification cost-effective on-chain?&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  #1 "I'm missing a real use case. I don't know where I'd use this."
&lt;/h2&gt;

&lt;p&gt;Two people asked for the same thing: show me a real-world example, not an abstract circuit.&lt;/p&gt;

&lt;p&gt;You're checking into a hotel. They need to verify your ID belongs to an adult. In Poland there are ID cards issued to minors (dowod osobisty dla osoby niepelnoletniej), and you can't tell from the document number alone whether the holder is 18+.&lt;/p&gt;

&lt;p&gt;With a ZK proof, the hotel verifies: "this person has a valid government-issued ID, and the date of birth in that ID satisfies age &amp;gt;= 18." The hotel sees yes or no. No date of birth, no name, no document number.&lt;/p&gt;

&lt;p&gt;This is a range check: prove that (current_date - date_of_birth) &amp;gt;= 6570 days without revealing date_of_birth. Range checks are one of the most basic operations you can build in a circuit (see question #6 for how they work), and age verification is one of the most practical applications.&lt;/p&gt;

&lt;p&gt;Technically: you have your digital ID (e-dowod) on your phone. At check-in the phone generates a ZK proof locally and presents it as a QR code. The hotel scans the code, verifies the proof, gets the yes/no answer. Everything happens locally, no server required.&lt;/p&gt;

&lt;p&gt;This isn't theoretical. The EU Digital Identity Wallet (eIDAS 2.0) is designed to support exactly this kind of selective disclosure. mObywatel 3.0 is planned for late 2026 as part of this framework (more on mObywatel in question #5).&lt;/p&gt;




&lt;h2&gt;
  
  
  #2 "How can I know what's in the next row of the execution trace if I'm only at the current one?"
&lt;/h2&gt;

&lt;p&gt;This is the most common confusion: people think proving IS executing. It's not. These are two separate steps.&lt;/p&gt;

&lt;p&gt;First, you do the execution. The prover takes the program, runs it on the inputs, and records every intermediate value into a table. That table is the execution trace. At this point the prover knows everything: every input, every output, every step.&lt;/p&gt;

&lt;p&gt;Then you do the proving. The prover takes the completed trace with all constraints and turns it into a proof. You don't need to "know the next row" during proving, because the full trace is already there.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The &lt;a href="https://rustarians.com/meetup-qa-krakow/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; includes the full execution trace table from my talk slides, showing a range check for proving that 42 is between 10 and 100 by decomposing the differences into bits.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  #3 "Do I need to learn the fundamentals first, and where?"
&lt;/h2&gt;

&lt;p&gt;Best book to start: &lt;a href="https://people.cs.georgetown.edu/jthaler/ProofsArgsAndZK.html" rel="noopener noreferrer"&gt;"Proofs, Arguments, and Zero-Knowledge"&lt;/a&gt; by Justin Thaler. People in the audience were taking photos when I showed it.&lt;/p&gt;

&lt;p&gt;You don't need to read the whole thing. Chapters 1-4 give you the foundation: randomized algorithms, interactive proofs, and the sum-check protocol. Polynomial commitments come later (chapters 14-16), but without the foundation from 1-4 the rest won't make sense.&lt;/p&gt;

&lt;p&gt;If you'd rather skip the book: my posts on &lt;a href="https://rustarians.com" rel="noopener noreferrer"&gt;rustarians.com&lt;/a&gt; start from zero (polynomials, roots of unity, execution traces) and include interactive code.&lt;/p&gt;




&lt;h2&gt;
  
  
  #4 "Why can't I just use a comparison? Why only multiplication and addition?"
&lt;/h2&gt;

&lt;p&gt;Because a circuit is not a program. In regular code you have &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;else&lt;/code&gt;, comparisons, loops. In an arithmetic circuit the building blocks are addition and multiplication over field elements. In PLONKish systems like halo2 you can define custom gates, which combine multiple additions and multiplications into a single constraint (e.g. &lt;code&gt;a * (b + c) = d&lt;/code&gt; in one gate). But the building blocks are still the same.&lt;/p&gt;

&lt;p&gt;Why can't you just compare? You might think it's because finite fields have no ordering. But even over integers (which do have ordering), you'd have the same problem: comparison is not a polynomial operation. You can't write &lt;code&gt;a &amp;lt; b&lt;/code&gt; as a polynomial equation. And every constraint ultimately has to be a polynomial, because that's what fits into the polynomial commitment scheme that makes the proof work.&lt;/p&gt;

&lt;p&gt;So you decompose comparisons into operations the circuit can express. For example, &lt;code&gt;a &amp;lt; 256&lt;/code&gt; becomes: check that &lt;code&gt;a&lt;/code&gt; fits in 8 bits. You take &lt;code&gt;a&lt;/code&gt;, decompose it into bits (b0, b1, ..., b7), check that each bit is 0 or 1 (&lt;code&gt;bit * (1 - bit) == 0&lt;/code&gt;), and that the sum &lt;code&gt;b0 + 2*b1 + 4*b2 + ... + 128*b7 == a&lt;/code&gt;. All additions and multiplications. The proof boils down to: the number can be expressed as a polynomial encoding its bits, and that polynomial satisfies all the constraints.&lt;/p&gt;




&lt;h2&gt;
  
  
  #5 "How would mObywatel work with ZK?"
&lt;/h2&gt;

&lt;p&gt;Same use case as question #1, but now the question is how to build it. You have a digital ID on your phone (mObywatel). You want to prove you're 18+ without revealing your date of birth or any other personal data.&lt;/p&gt;

&lt;p&gt;The prover (your phone) puts three things into the circuit: your certificate from mObywatel, the government's public key, and the age constraint (age &amp;gt;= 18). The circuit verifies the signature on the certificate, extracts the date of birth, checks the constraint, and produces a proof.&lt;/p&gt;

&lt;p&gt;The verifier (the hotel) takes three things: the proof, the government's public key, and the same age constraint. That's it. No certificate, no personal data, just the proof and the public inputs.&lt;/p&gt;

&lt;p&gt;If I were building this, I'd use Schnorr or EdDSA for the document signature, not RSA. Schnorr verification in a ZK circuit costs ~10-11k constraints, RSA-2048 costs ~536k, roughly 50x more. The choice of cryptographic primitives matters in ZK: pick the wrong ones and your proof size and proving time explode.&lt;/p&gt;




&lt;h2&gt;
  
  
  #6 "Can I use bounds larger than 8 bits in the example?"
&lt;/h2&gt;

&lt;p&gt;Yes, but the cost grows linearly with the number of bits. 8 bits = 8 bit decomposition gates + 1 for the sum. And for a range check you need two decompositions: one for the lower bound, one for the upper bound. So 8-bit range = 2 x 8 bit decomposition gates. 16 bits = 2 x 16. 256 bits = 2 x 256.&lt;/p&gt;

&lt;p&gt;For large ranges, bit decomposition gets expensive. You can use lookup tables instead: you have a table of allowed values and check whether your value is in the table. halo2 has built-in lookup tables, and for large ranges they're faster than bit decomposition.&lt;/p&gt;




&lt;h2&gt;
  
  
  #7 "Can you formally prove a circuit is correct, e.g. with Lean?"
&lt;/h2&gt;

&lt;p&gt;Yes, and this is not theory. People already do it on production circuits.&lt;/p&gt;

&lt;p&gt;Nethermind built &lt;a href="https://www.nethermind.io/blog/formal-verification-of-halo2-circuits-in-lean" rel="noopener noreferrer"&gt;Halva&lt;/a&gt;: a framework for formal verification of halo2 circuits in Lean. They used it on Scroll's Keccak-256 circuit and found a critical bug in production.&lt;/p&gt;

&lt;p&gt;The same team built &lt;a href="https://www.nethermind.io/blog/formally-verifying-zero-knowledge-circuits-introducing-certiplonk" rel="noopener noreferrer"&gt;CertiPlonk&lt;/a&gt; for formalizing the PlonK protocol IOP in Lean, and &lt;a href="https://blog.succinct.xyz/formal-verification-of-sp1-with-lean/" rel="noopener noreferrer"&gt;sp1-lean&lt;/a&gt; together with Succinct Labs for verifying chips from SP1 zkVM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cs.utexas.edu/~isil/coda.pdf" rel="noopener noreferrer"&gt;CODA&lt;/a&gt; (from UT Austin) uses Coq and verified 77 existing circom circuits. They found 6 new vulnerabilities nobody had noticed before.&lt;/p&gt;

&lt;p&gt;Lean has become the dominant proof assistant for ZK. For a comparison of frameworks, see this &lt;a href="https://blog.zksecurity.xyz/posts/formal-verification-arithmetic-circuits/" rel="noopener noreferrer"&gt;zkSecurity overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is still expensive though. Each circuit needs a separate proof, and verifying a production circuit takes weeks or months.&lt;/p&gt;

&lt;p&gt;What people do when they don't have the budget for formal verification: property-based testing (generate random inputs and check the circuit behaves correctly) + &lt;code&gt;MockProver&lt;/code&gt; in halo2 (catches under-constrained circuits during development). Property-based testing won't catch all corner cases, but it's much cheaper than a formal proof and catches most of the obvious bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  #8 "Is what you showed a SNARK or a STARK?"
&lt;/h2&gt;

&lt;p&gt;It depends on what you mean by SNARK. halo2 is a proof system based on PLONKish arithmetization with polynomial commitments. Whether it's a "SNARK" depends on which backend you use and how strictly you define the "S."&lt;/p&gt;

&lt;p&gt;SNARK stands for Succinct Non-interactive ARgument of Knowledge. "Succinct" means small proof and fast verification. With &lt;strong&gt;KZG&lt;/strong&gt; commitments (used by Scroll, PSE), halo2 is a SNARK: O(1) proof size, O(1) verification, but requires a trusted setup.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;IPA&lt;/strong&gt; (Inner Product Argument), the proof is O(log n), which is small, but verification requires a linear-time multi-scalar multiplication: O(n). So the proof is succinct, but verification is not. The original &lt;a href="https://eprint.iacr.org/2019/1021" rel="noopener noreferrer"&gt;Halo paper&lt;/a&gt; avoids calling it a SNARK, using "non-interactive argument of knowledge" instead. In practice, people call it a SNARK anyway because the proof is small and the &lt;a href="https://vitalik.eth.limo/general/2021/11/05/halo.html" rel="noopener noreferrer"&gt;accumulation scheme&lt;/a&gt; amortizes the linear verification cost across recursive steps. But strictly speaking: IPA verification is not succinct.&lt;/p&gt;

&lt;h3&gt;
  
  
  STARK (Scalable Transparent ARgument of Knowledge)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Larger proof (tens of KB)&lt;/li&gt;
&lt;li&gt;No trusted setup (transparent)&lt;/li&gt;
&lt;li&gt;Quantum-resistant (based on hashes, not curves)&lt;/li&gt;
&lt;li&gt;Prover is usually faster for large circuits, but verification is slower and the proof is bigger. That's why RISC Zero wraps its STARK proof in a Groth16 SNARK for on-chain verification&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  #9 "ZK in electronic voting"
&lt;/h2&gt;

&lt;p&gt;This works through nullifiers. Each voter has a secret key, and from that key and the election ID they compute a hash called the nullifier. If someone tries to vote twice, the same nullifier already exists and the transaction gets rejected, but nobody can tell whose nullifier it is because hashing is one-way. &lt;a href="https://z.cash/the-basics/" rel="noopener noreferrer"&gt;Zcash&lt;/a&gt; uses the same mechanism for private transactions and &lt;a href="https://docs.semaphore.pse.dev/" rel="noopener noreferrer"&gt;Semaphore&lt;/a&gt; uses it for anonymous signaling.&lt;/p&gt;

&lt;p&gt;An eVoting system with ZK works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Registration:&lt;/strong&gt; each eligible voter gets a secret key. Their public key goes into a merkle tree on-chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voting:&lt;/strong&gt; the voter creates a ZK proof: "my key is in the merkle tree of eligible voters, I vote for option X, here's my nullifier."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification:&lt;/strong&gt; the smart contract checks the proof, checks the nullifier hasn't been used, records the vote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counting:&lt;/strong&gt; votes are public (option A: 47, option B: 53), but who voted for what is hidden.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Projects that do this: &lt;a href="https://maci.pse.dev" rel="noopener noreferrer"&gt;MACI&lt;/a&gt; (Minimal Anti-Collusion Infrastructure, concept by Vitalik, built by Privacy &amp;amp; Scaling Explorations / Ethereum Foundation, actively developed, v3 on the way), &lt;a href="https://vocdoni.io" rel="noopener noreferrer"&gt;Vocdoni&lt;/a&gt; (building DAVINCI, a zkRollup for voting with recursive proofs), and &lt;a href="https://docs.semaphore.pse.dev/" rel="noopener noreferrer"&gt;Semaphore&lt;/a&gt;-based voting.&lt;/p&gt;

&lt;p&gt;The hard part is coercion resistance: how do you guarantee nobody is forcing you to vote a certain way? MACI solves this through key rotation. You can change your key after voting, so even if someone saw your vote, they don't know if you changed it afterwards.&lt;/p&gt;




&lt;h2&gt;
  
  
  #10 "Is ZK verification cost-effective on-chain?"
&lt;/h2&gt;

&lt;p&gt;General rule: verifying a ZK proof on-chain is cheap (compared to executing the full computation on-chain). Generating the proof is expensive (off-chain, on the prover's CPU/GPU).&lt;/p&gt;

&lt;p&gt;Some numbers: a &lt;a href="https://hackmd.io/@nebra-one/ByoMB8Zf6" rel="noopener noreferrer"&gt;Groth16 verification&lt;/a&gt; on Ethereum costs roughly 200k-300k gas depending on the number of public inputs (formula: ~181k + 6k per public input). The underlying &lt;code&gt;ecPairing&lt;/code&gt; precompile (&lt;a href="https://eips.ethereum.org/EIPS/eip-1108" rel="noopener noreferrer"&gt;EIP-1108&lt;/a&gt;) costs 34,000 * k + 45,000 gas where k is the number of pairings. At 5 gwei and ETH at $2,500, that's roughly $3 per verification, less when gas is low.&lt;/p&gt;

&lt;p&gt;When ZK makes economic sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The computation is expensive on-chain but cheap off-chain (e.g. batch processing 1000 transactions, proved with a single proof)&lt;/li&gt;
&lt;li&gt;Privacy has value (user pays a premium for anonymity)&lt;/li&gt;
&lt;li&gt;Regulations require privacy (GDPR, personal data)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When ZK does not make sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple computations (cheaper to execute directly)&lt;/li&gt;
&lt;li&gt;You don't need privacy or compression&lt;/li&gt;
&lt;li&gt;Latency is critical (proving takes seconds to minutes)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Next Up
&lt;/h2&gt;

&lt;p&gt;I'm turning the meetup demo into a full interactive post: building a range check circuit in halo2, step by step, with runnable code. If you want to go from "I get the concept" to "I can write this," that's the one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;These are questions from the Krakow Rust meetup (27.02.2026). &lt;a href="https://rustarians.com/10-questions-from-a-zk-meetup-in-krakow/" rel="noopener noreferrer"&gt;Original post&lt;/a&gt; on rustarians.com.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm running a free live session on March 17: &lt;a href="https://rustarians.com/webinar/" rel="noopener noreferrer"&gt;From Polynomials to Proof in 60 Minutes&lt;/a&gt;. If you want to see a range check circuit built step by step, that's where it happens.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions? Found a bug? &lt;a href="https://www.linkedin.com/in/adam-smolarek/" rel="noopener noreferrer"&gt;linkedin.com/in/adam-smolarek&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cryptography</category>
      <category>zeroknowledge</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Execution Traces: Packing Data for Polynomials</title>
      <dc:creator>Adam</dc:creator>
      <pubDate>Tue, 10 Mar 2026 11:16:39 +0000</pubDate>
      <link>https://forem.com/rustarians/execution-traces-packing-data-for-polynomials-9cf</link>
      <guid>https://forem.com/rustarians/execution-traces-packing-data-for-polynomials-9cf</guid>
      <description>&lt;p&gt;&lt;em&gt;This post has interactive code editors and animations on the &lt;a href="https://rustarians.com/execution-trace/" rel="noopener noreferrer"&gt;original version&lt;/a&gt; where you can run and modify every Rust snippet in the browser.&lt;/em&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Execution Traces: Packing Data for Polynomials
&lt;/h1&gt;

&lt;p&gt;Welcome, fellow zk-enjoyer.&lt;/p&gt;

&lt;p&gt;If you've been following along, you know how polynomials work as a data structure and why roots of unity make evaluating them fast. Now comes the question everyone skips: before the polynomial checks anything, where does the data come from?&lt;/p&gt;

&lt;p&gt;This is post #3 in the "first principles" ZK series:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://rustarians.com/polynomials-in-zk-snarks/" rel="noopener noreferrer"&gt;Polynomials and finite fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rustarians.com/roots-of-unity/" rel="noopener noreferrer"&gt;Roots of unity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution traces&lt;/strong&gt; &amp;lt;- you are here&lt;/li&gt;
&lt;li&gt;FFT/NTT interpolation&lt;/li&gt;
&lt;li&gt;Constraints&lt;/li&gt;
&lt;li&gt;Polynomial packing&lt;/li&gt;
&lt;li&gt;KZG commitment&lt;/li&gt;
&lt;li&gt;Proof and verification&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  You already know how to do this
&lt;/h2&gt;

&lt;p&gt;Here's a function you've written a hundred times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_valid_age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CPU runs it, checks the condition, returns true or false. The result is immediate. You trust the machine.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. Poland's digital ID (e-dowod) on mobile phones could generate exactly this kind of proof: a hotel checks you're 18+ from a QR code without seeing your birth date. The circuit behind it is a range check like the one we're building here. (&lt;a href="https://rustarians.com/meetup-qa-krakow/" rel="noopener noreferrer"&gt;More on real-world ZK use cases in the Krakow meetup Q&amp;amp;A.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Now imagine you need to prove to someone that this returned &lt;code&gt;true&lt;/code&gt; for a specific value of &lt;code&gt;x&lt;/code&gt;, without revealing &lt;code&gt;x&lt;/code&gt;. The verifier can't run your code on your private inputs. They get a proof, a small piece of data, and they verify it mathematically.&lt;/p&gt;

&lt;p&gt;To produce that proof, you need an execution trace.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is an execution trace?
&lt;/h2&gt;

&lt;p&gt;An execution trace is a table. Each row captures a value that was relevant to your computation. You fill it in while running the program, before any proving happens.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;is_valid_age(45)&lt;/code&gt;, the two conditions &lt;code&gt;x &amp;gt;= 18&lt;/code&gt; and &lt;code&gt;x &amp;lt;= 90&lt;/code&gt; get rewritten as subtractions: &lt;code&gt;x - 18&lt;/code&gt; (should be non-negative if x &amp;gt;= 18) and &lt;code&gt;90 - x&lt;/code&gt; (should be non-negative if x &amp;lt;= 90). This rewriting is part of translating code into polynomials. It's usually the hardest part of writing circuits, because the equations have to catch every corner case and enforce the same conditions as the original code. More on how constraints work in later posts. For now, these are the values we track:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;step&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x - 18&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90 - x&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's it. You're writing down every intermediate value so a polynomial can encode them later. The polynomial doesn't execute your logic. It encodes this table, and constraints check that the numbers are consistent.&lt;/p&gt;

&lt;p&gt;OK, so we rewrote the comparison from the &lt;code&gt;if&lt;/code&gt; as subtractions. But we still need to check that the result is non-negative, which looks like the same kind of check we started with. It is, almost, but not quite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just compare directly?
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting.&lt;/p&gt;

&lt;p&gt;ZK proofs operate over finite fields. In a finite field with modulus p, every number is in the range [0, p-1] and arithmetic wraps around. There is no concept of "greater than" or "less than" in the algebraic sense.&lt;/p&gt;

&lt;p&gt;This means &lt;code&gt;x - 18 &amp;gt;= 0&lt;/code&gt; is meaningless. If &lt;code&gt;x = 5&lt;/code&gt;, then &lt;code&gt;x - 18&lt;/code&gt; doesn't equal &lt;code&gt;-13&lt;/code&gt;. It equals &lt;code&gt;p - 13&lt;/code&gt;, which is a large positive-looking number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// invalid: x = 5, below lower bound&lt;/span&gt;
    &lt;span class="c1"&gt;// in F_101: 5 - 18 = -13, and -13 mod 101 = 88&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x=5:  x-18 mod p = {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;18_i64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.rem_euclid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 88&lt;/span&gt;

    &lt;span class="c1"&gt;// valid: x = 45, inside [18, 90]&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x=45: x-18 mod p = {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;18_i64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.rem_euclid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 27&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both results look nonnegative, the field can't tell them apart by sign.&lt;/p&gt;

&lt;p&gt;So at this point it looks like rewriting comparisons as subtractions and checking for non-negative results was completely pointless. In a finite field every result looks non-negative. Bear with me, we're getting there. (If you want more on why circuits only speak addition and multiplication, see &lt;a href="https://rustarians.com/meetup-qa-krakow/" rel="noopener noreferrer"&gt;Q4 in the meetup Q&amp;amp;A&lt;/a&gt;.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Do you even range-check?
&lt;/h2&gt;

&lt;p&gt;You use bit decomposition.&lt;/p&gt;

&lt;p&gt;Wait what???&lt;/p&gt;

&lt;p&gt;You take the result of the subtraction and break it down into bits. If it fits in k bits, you know it's a small number. If it doesn't fit, it's huge (because the finite field wrapped it around, and those wrapped numbers are huuuge), and the check fails. That's it. 8 bits can hold 0 to 255, so if x - 18 fits in 8 bits, x is somewhere between 18 and 273.&lt;/p&gt;

&lt;p&gt;One check isn't enough. You need two: one for the lower bound (&lt;code&gt;x - 18&lt;/code&gt; fits in k bits, so x &amp;gt;= 18) and one for the upper bound (&lt;code&gt;90 - x&lt;/code&gt; fits in k bits, so x &amp;lt;= 90). Both together prove x is between 18 and 90. That's why the trace ends up with two bit decompositions, one per bound.&lt;/p&gt;

&lt;p&gt;The gap between 18 and 90 is 72, so 8 bits is more than enough (8 bits can hold up to 255). But each check alone has a range that is too wide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;col_lower alone accepts x from 18 up to 273 (because x - 18 just needs to fit in 8 bits)&lt;/li&gt;
&lt;li&gt;col_upper alone accepts x from -165 up to 90 (because 90 - x just needs to fit in 8 bits)&lt;/li&gt;
&lt;li&gt;both together accept only x from 18 to 90&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try x = 91 (one above the upper bound). col_lower: 91 - 18 = 73, fits in 8 bits, passes. col_upper: 90 - 91 = p - 1, a ~254-bit number, does not fit in 8 bits, fails. The lower check alone would have let x = 91 through. You need both.&lt;/p&gt;

&lt;p&gt;Try x = 17 (one below the lower bound). col_upper: 90 - 17 = 73, fits, passes. col_lower: 17 - 18 = p - 1, does not fit in 8 bits, fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// x = 91 (one above upper bound)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;91u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x = 91:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  x - 18 = {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18u64&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;       &lt;span class="c1"&gt;// 73, small&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  90 - x = {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;90u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// huge wrapped number&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// x = 17 (one below lower bound)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x = 17:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  x - 18 = {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18u64&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;       &lt;span class="c1"&gt;// huge wrapped number&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  90 - x = {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;90u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// 73, small&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For x = 91, the lower check gives 73 (small, fits in 8 bits), but the upper check wraps around to a ~254-bit number. For x = 17, it's the other way around. Those giant numbers will never fit in 8 bits.&lt;/p&gt;

&lt;p&gt;Here's why that's a hard constraint, not just a flag. To prove a k-bit decomposition, the prover has to provide k individual bits, each either 0 or 1, that add up to the original value when weighted by powers of two. For 27 that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1*1 + 1*2 + 0*4 + 1*8 + 1*16 + 0*32 + 0*64 + 0*128 = 27
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most you can reach with 8 bits is 1+2+4+8+16+32+64+128 = 255. If the value is 256 or more, no combination of 0s and 1s adds up to it. So we effectively have a non-negative check that works over a finite field, in the form of a polynomial: if the number is genuinely small (a valid subtraction result), it can be represented as the polynomial above. If the field wrapped it around into a giant number, it can't.&lt;br&gt;
There's one more thing needed: a constraint that forces each bit to actually be 0 or 1, not some other field element. More on that in the constraints post.&lt;/p&gt;


&lt;h2&gt;
  
  
  The full trace
&lt;/h2&gt;

&lt;p&gt;Now you can see why the trace looks the way it does. For &lt;code&gt;is_valid_age(45)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;row&lt;/th&gt;
&lt;th&gt;col_x&lt;/th&gt;
&lt;th&gt;col_lower (x-18)&lt;/th&gt;
&lt;th&gt;col_upper (90-x)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;col_x&lt;/code&gt; is there because without it there's no way to prove that &lt;code&gt;col_lower[0]&lt;/code&gt; actually equals &lt;code&gt;x - 18&lt;/code&gt; and not some arbitrary value. Same for &lt;code&gt;col_upper[0]&lt;/code&gt; and &lt;code&gt;90 - x&lt;/code&gt;.&lt;br&gt;
You didn't write &lt;code&gt;if&lt;/code&gt;. You wrote down what had to be true for the &lt;code&gt;if&lt;/code&gt; to pass, for specific case of secret value x.&lt;/p&gt;


&lt;h2&gt;
  
  
  In arkworks: the trace as field elements
&lt;/h2&gt;

&lt;p&gt;Throughout this series we build PLONK by hand using &lt;code&gt;ark-ff&lt;/code&gt;, just field arithmetic. Each cell in the trace is a field element.&lt;/p&gt;

&lt;p&gt;The snippet below computes the subtraction results and breaks them into bits, the same bit decomposition from the previous section, but now in arkworks code. &lt;code&gt;to_bits_le&lt;/code&gt; extracts k least-significant bits from a field element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;to_bits_le&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// first u64 limb, valid for small values&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bits_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;to_bits_le&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower_bound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// [1, 1, 0, 1, 1, 0, 0, 0]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bits_upper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;to_bits_le&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// [1, 0, 1, 1, 0, 1, 0, 0]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we assemble the trace. Each column holds the value in row 0 followed by its 8 bits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// each column: [value, b0, b1, b2, b3, b4, b5, b6, b7]&lt;/span&gt;
&lt;span class="c1"&gt;// col_x carries x itself, needed to prove col_lower[0] = x - 18&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;col_x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;           &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&lt;span class="nf"&gt;.concat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;col_lower&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lower_bound&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="nf"&gt;to_bits_le&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower_bound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="nf"&gt;.concat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;col_upper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;to_bits_le&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="nf"&gt;.concat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col_lower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col_upper&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visualized:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;row&lt;/th&gt;
&lt;th&gt;col_x&lt;/th&gt;
&lt;th&gt;col_lower (x-18)&lt;/th&gt;
&lt;th&gt;col_upper (90-x)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What the trace is for
&lt;/h2&gt;

&lt;p&gt;"OK I built this trace, but what's it for?"&lt;/p&gt;

&lt;p&gt;Glad you asked.&lt;/p&gt;

&lt;p&gt;A common confusion: people think proving IS executing. It's not. These are two separate steps. First, the prover runs the program on the inputs and records every intermediate value into a table, that's exactly what the first snippet in the arkworks section does. That's the execution trace. At this point the prover knows everything: every input, every output, every step.&lt;/p&gt;

&lt;p&gt;Then the prover takes the completed trace and turns it into a proof. The constraints don't walk through the execution trace row by row, discovering what happens next. The full trace is already there. They just check that all the numbers are correct. This separation is what allows the verifier to check correctness without re-running the computation.&lt;/p&gt;

&lt;p&gt;You pack the data, and the constraints check it. AND you can pack the data if and only if constraints are satisfied.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;Post #4, FFT/NTT interpolation: how to turn this table into polynomials. And do some cool stuff with it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Coming soon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The FFT/NTT algorithm is covered in &lt;a href="https://rustarians.com/roots-of-unity/" rel="noopener noreferrer"&gt;Roots of Unity&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you have questions or want to discuss this post, join the &lt;a href="https://rustarians.com/dc" rel="noopener noreferrer"&gt;ZK for Rust Devs Discord&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is post #3 in my "first principles" ZK series for Rust devs. &lt;a href="https://rustarians.com/execution-trace/" rel="noopener noreferrer"&gt;Interactive version with runnable code and animations&lt;/a&gt; on rustarians.com.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm running a free live session on March 17: &lt;a href="https://rustarians.com/webinar/" rel="noopener noreferrer"&gt;From Polynomials to Proof in 60 Minutes&lt;/a&gt;. Range check circuit built step by step, with a real use case.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions? Found a bug? &lt;a href="https://www.linkedin.com/in/adam-smolarek/" rel="noopener noreferrer"&gt;linkedin.com/in/adam-smolarek&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cryptography</category>
      <category>zeroknowledge</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What are Roots of Unity (and why do they matter in ZK)?</title>
      <dc:creator>Adam</dc:creator>
      <pubDate>Tue, 10 Mar 2026 11:15:07 +0000</pubDate>
      <link>https://forem.com/rustarians/what-are-roots-of-unity-and-why-do-they-matter-in-zk-3b63</link>
      <guid>https://forem.com/rustarians/what-are-roots-of-unity-and-why-do-they-matter-in-zk-3b63</guid>
      <description>&lt;p&gt;&lt;em&gt;This post has interactive code editors and animations on the &lt;a href="https://rustarians.com/roots-of-unity/" rel="noopener noreferrer"&gt;original version&lt;/a&gt; where you can run and modify every Rust snippet in the browser.&lt;/em&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Roots of Unity
&lt;/h1&gt;

&lt;p&gt;Welcome back, fellow zk-enjoyer.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://rustarians.com/polynomials-in-zk-snarks/" rel="noopener noreferrer"&gt;the last post&lt;/a&gt;, we used polynomials as a data structure and verified them with a single number (Schwartz-Zippel, remember?). But there's a question that should be bugging you: "how do we do this &lt;em&gt;efficiently&lt;/em&gt;?"&lt;/p&gt;

&lt;p&gt;The answer is called "roots of unity." I promise the idea is simpler than the name suggests, and it's what makes ZK proofs better (about 50,000 times better, for a typical ZK circuit, terms and conditions may apply [TM]).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Road Map
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Polynomials over finite fields&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Roots of unity&lt;/strong&gt; &amp;lt;- you are here&lt;/li&gt;
&lt;li&gt; Execution trace&lt;/li&gt;
&lt;li&gt; Polynomial interpolation via FFT/NTT&lt;/li&gt;
&lt;li&gt; Applying constraints to polynomials&lt;/li&gt;
&lt;li&gt; Packing into a single polynomial&lt;/li&gt;
&lt;li&gt; Commitment scheme (starting with KZG)&lt;/li&gt;
&lt;li&gt; Proof AND Verification&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why Are There Two Ways to Store a Polynomial?
&lt;/h2&gt;

&lt;p&gt;Before we get to roots of unity, you need to see the problem they solve.&lt;/p&gt;

&lt;p&gt;You can represent a polynomial in two different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coefficient form&lt;/strong&gt; is what you already know: store the coefficients as a vector. P(x) = 1 + 2x + 3x^2 becomes &lt;code&gt;[1, 2, 3]&lt;/code&gt;. Adding two polynomials is cheap: add the vectors element by element, O(N). But multiplying them? Every coefficient of P has to multiply with every coefficient of Q. That's O(N^2).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation form&lt;/strong&gt; stores what the polynomial outputs at N chosen points. Instead of &lt;code&gt;[1, 2, 3]&lt;/code&gt;, you store [P(x_0), P(x_1), ..., P(x_{n-1})]. Now multiplying two polynomials is trivial: just multiply the values at each point, O(N).&lt;/p&gt;

&lt;p&gt;Think of it like a coin. Coefficient form and evaluation form are two views of the same polynomial. Each view makes a different operation cheap: coefficients for addition, evaluations for multiplication. The catch is flipping the coin.&lt;/p&gt;

&lt;p&gt;Flipping from coefficients to points is &lt;em&gt;evaluation&lt;/em&gt;: plug N points into the polynomial, O(N^2). Flipping back is &lt;em&gt;interpolation&lt;/em&gt;: find the polynomial that passes through N given points, also O(N^2).&lt;/p&gt;

&lt;p&gt;For N = 2^20 coefficients (a common ZK domain size), each flip costs (2^20)^2 = 2^40: about a trillion multiplications. Evaluation form is where the heavy operations happen, but the flips are the bottleneck.&lt;/p&gt;

&lt;p&gt;Unless you pick the right evaluation points.&lt;/p&gt;




&lt;h2&gt;
  
  
  So What Are Roots of Unity?
&lt;/h2&gt;

&lt;p&gt;For the 8 roots of unity, you start with a number omega. Then omega^0 = 1, omega^1, omega^2, ..., omega^7 gives you 8 distinct values, and omega^8 = 1 again.&lt;/p&gt;

&lt;p&gt;On the complex plane, roots of unity sit on a circle and multiplying by omega is a visible rotation. That's the intuition.&lt;/p&gt;

&lt;p&gt;In a finite field, there's no circle: omega is a single integer (a big one), and "multiply by omega" is just modular multiplication. Nothing rotates. But the algebra is the same: N distinct values that cycle back to 1, with identical symmetries. The circle helps you see the structure. The code below is how roots of unity actually look in ZK.&lt;/p&gt;

&lt;p&gt;But in many zkSNARK frameworks and libraries you will see a thing named omega and operations called rotation. This makes no sense for integers. The name comes from the complex plane, where it does make sense, and that's why those operations and primitives are named that way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FftField&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The primitive 8th root of unity for BN254.&lt;/span&gt;
    &lt;span class="c1"&gt;// "Primitive" means: omega^8 = 1,&lt;/span&gt;
    &lt;span class="c1"&gt;// and omega^k != 1 for 0 &amp;lt; k &amp;lt; 8.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_root_of_unity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Field has this root"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"All 8th roots of unity for BN254:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// start at omega^0 = 1&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  omega^{}  = {:?}  &amp;lt;-- back to 1!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  omega^{}  = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those numbers are huge (welcome to finite fields), but the punchline is at the bottom: omega^0 and omega^8 are the same value. The cycle is the same as in the complex plane, and it also has exactly 8 distinct values.&lt;/p&gt;

&lt;p&gt;There's one more thing to see here. The "root" in "root of unity" means the same as in "root of an equation": a number that makes the equation equal zero. Every root of unity satisfies omega^8 = 1, or equivalently omega^8 - 1 = 0. Not just one omega, but &lt;em&gt;every&lt;/em&gt; root:&lt;/p&gt;

&lt;p&gt;(omega^0)^8 = 1&lt;br&gt;
(omega^1)^8 = 1&lt;br&gt;
(omega^2)^8 = 1&lt;br&gt;
...&lt;br&gt;
(omega^7)^8 = 1&lt;/p&gt;

&lt;p&gt;That's the definition. Why? Because you can reorder the exponents:&lt;/p&gt;

&lt;p&gt;(omega^2)^8 = (omega^8)^2 = 1^2 = 1&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FftField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_root_of_unity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Field has this root"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Every 8th root of unity, raised to the 8th power:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="nf"&gt;.pow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;       &lt;span class="c1"&gt;// first: omega^k&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;omega_k&lt;/span&gt;&lt;span class="nf"&gt;.pow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;      &lt;span class="c1"&gt;// then: (omega^k)^8&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  (omega^{})^8 = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;All equal 1. They're all roots of z^8 - 1 = 0."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will matter later: the polynomial z^8 - 1 has &lt;em&gt;all eight&lt;/em&gt; roots of unity as its roots, which means that no matter what omega you put into that equation, you will always get 0.&lt;/p&gt;

&lt;p&gt;That fact gives us a way to check constraints across all roots at once, and of course we can use roots of unity as the x coordinate for encoding our data, instead of one by one (post #5).&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does This Speed Things Up?
&lt;/h2&gt;

&lt;p&gt;Take a polynomial P(x) and split it by even and odd coefficients:&lt;/p&gt;

&lt;p&gt;P(x) = P_even(x^2) + x * P_odd(x^2)&lt;/p&gt;

&lt;p&gt;For example, take a degree-7 polynomial with 8 coefficients:&lt;/p&gt;

&lt;p&gt;P(x) = 3 + 1x + 4x^2 + 1x^3 + 5x^4 + 9x^5 + 2x^6 + 6x^7&lt;/p&gt;

&lt;p&gt;Pull out the even-index coefficients (positions 0, 2, 4, 6) and the odd-index coefficients (positions 1, 3, 5, 7):&lt;/p&gt;

&lt;p&gt;P_even(y) = 3 + 4y + 5y^2 + 2y^3&lt;br&gt;
P_odd(y) = 1 + 1y + 9y^2 + 6y^3&lt;/p&gt;

&lt;p&gt;Plug in y = x^2 and recombine:&lt;/p&gt;

&lt;p&gt;P(x) = (3 + 4x^2 + 5x^4 + 2x^6) + x * (1 + 1x^2 + 9x^4 + 6x^6)&lt;/p&gt;

&lt;p&gt;One degree-7 polynomial just became two degree-3 polynomials. Each of those can be split the same way. Split P_even by its even/odd coefficients:&lt;/p&gt;

&lt;p&gt;P_even_even(y) = 3 + 5y&lt;br&gt;
P_even_odd(y) = 4 + 2y&lt;/p&gt;

&lt;p&gt;And split P_odd the same way:&lt;/p&gt;

&lt;p&gt;P_odd_even(y) = 1 + 9y&lt;br&gt;
P_odd_odd(y) = 1 + 6y&lt;/p&gt;

&lt;p&gt;Four degree-1 polynomials. Split once more, each has two coefficients, so the split produces eight degree-0 polynomials (just the constants: 3, 5, 4, 2, 1, 9, 1, 6).&lt;/p&gt;

&lt;p&gt;Eight constants. "Evaluation" of a degree-0 polynomial is just returning the number. That's the base case.&lt;/p&gt;

&lt;p&gt;The full cascade: 8 -&amp;gt; 4 -&amp;gt; 2 -&amp;gt; 1 coefficients per sub-polynomial, log2(8) = 3 levels of splitting.&lt;/p&gt;

&lt;p&gt;Now the computation runs in reverse, from the bottom up. At each level, the butterfly combines two values a and b into a + omega^k * b and a - omega^k * b.&lt;/p&gt;

&lt;p&gt;The omega^k multiplier is called the &lt;strong&gt;twiddle factor&lt;/strong&gt;: it's just a power of omega that changes at each level. At the first level it's 1, so the butterfly simplifies to plain a + b and a - b:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1: evaluate degree-1 polynomials at 2nd roots (omega^k = 1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;P_even_even(omega^0) = 3 + 1*5 = 8, P_even_even(omega^4) = 3 - 1*5 = -2&lt;br&gt;
P_even_odd(omega^0) = 4 + 1*2 = 6, P_even_odd(omega^4) = 4 - 1*2 = 2&lt;br&gt;
P_odd_even(omega^0) = 1 + 1*9 = 10, P_odd_even(omega^4) = 1 - 1*9 = -8&lt;br&gt;
P_odd_odd(omega^0) = 1 + 1*6 = 7, P_odd_odd(omega^4) = 1 - 1*6 = -5&lt;/p&gt;

&lt;p&gt;All additions and subtractions, the twiddle factor at this level is just 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2: combine Level 1 results into degree-3 polynomials (twiddle factors: 1 and omega^2)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;P_even(omega^0) = 8 + 1*6 = 14, P_even(omega^4) = 8 - 1*6 = 2&lt;br&gt;
P_even(omega^2) = -2 + omega^2 * 2, P_even(omega^6) = -2 - omega^2 * 2&lt;br&gt;
P_odd(omega^0) = 10 + 1*7 = 17, P_odd(omega^4) = 10 - 1*7 = 3&lt;br&gt;
P_odd(omega^2) = -8 + omega^2 * (-5), P_odd(omega^6) = -8 - omega^2 * (-5)&lt;/p&gt;

&lt;p&gt;Same butterfly, but now the twiddle factor alternates between 1 and omega^2. Where it's 1, we get clean numbers. Where it's omega^2, the expression stays symbolic, but in a finite field omega is a concrete integer, so these all resolve to specific values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3: combine Level 2 results into the full degree-7 polynomial (twiddle factors: 1, omega, omega^2, omega^3)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;P(omega^0) = 14 + 1*17 = 31, P(omega^4) = 14 - 1*17 = -3&lt;br&gt;
P(omega^1) = P_even(omega^2) + omega * P_odd(omega^2)&lt;br&gt;
P(omega^2) = 2 + omega^2 * 3, P(omega^6) = 2 - omega^2 * 3&lt;/p&gt;

&lt;p&gt;All eight evaluations, four twiddle factors. Each line: left and right share the same computation, the right just flips the sign. That's the butterfly: same work, two outputs.&lt;/p&gt;

&lt;p&gt;Why "butterfly"? Look at each crossing in the diagram: two lines cross and diverge, like a pair of wings. If you squint hard enough, you can see it. It would be a shame to waste such a cool name.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The &lt;a href="https://rustarians.com/roots-of-unity/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; has an interactive SVG butterfly diagram showing the full 8-point FFT with all three stages.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The pattern widens at each stage: stage 1 combines adjacent pairs, stage 2 reaches across two, stage 3 across four. At every crossing, two inputs produce two outputs, same computation, just + and -.&lt;/p&gt;

&lt;p&gt;For our 8-coefficient polynomial, that's 3 stages of 8 operations each, N * log2(N) total instead of N^2. That's O(N log N) vs O(N^2).&lt;/p&gt;

&lt;p&gt;Why does the halving work? omega^1 and omega^5 sit opposite each other on the circle, so squaring both gives the same result: (omega^1)^2 = (omega^5)^2 = omega^2. Eight roots collapse to four distinct squared values, four to two, two to one.&lt;/p&gt;

&lt;p&gt;Here's the full cascade: all 8 roots of unity, squaring down level by level. Each level, paired roots produce the same squared value, halving the distinct values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FftField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_root_of_unity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"8th root exists"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="nf"&gt;.pow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Square every root: opposite pairs collapse.&lt;/span&gt;
    &lt;span class="c1"&gt;// 8 inputs -&amp;gt; only 4 distinct squared values.&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"8 roots -&amp;gt; square -&amp;gt; 4 distinct values:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sq_k&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sq_opp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sq_k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sq_opp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  (omega^{})^2 == (omega^{})^2  check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Square again: 4 -&amp;gt; 2 distinct values.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;level1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;4 values -&amp;gt; square -&amp;gt; 2 distinct values:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;level1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;level1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;level1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  level1[{}]^2 == level1[{}]^2  check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Once more: 2 -&amp;gt; 1.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;level2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;level1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;level1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;level2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;level2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;level2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;2 values -&amp;gt; square -&amp;gt; 1 value  check"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three levels of halving, so half the computation at each level is shared with the level above.&lt;/p&gt;

&lt;p&gt;Here's the math for a real ZK domain:&lt;/p&gt;

&lt;p&gt;N = 2^20 (1,048,576 points, a common circuit size):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Naive: N^2 = 2^40 ~ 1.1 trillion field multiplications&lt;/li&gt;
&lt;li&gt;NTT: N * log2(N) = 2^20 * 20 ~ 21 million&lt;/li&gt;
&lt;li&gt;Speedup: ~50,000x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;N = 2^25 (~33 million points, large production circuits):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Naive: N^2 = 2^50 ~ 10^15&lt;/li&gt;
&lt;li&gt;NTT: N * log2(N) = 2^25 * 25 ~ 839 million&lt;/li&gt;
&lt;li&gt;Speedup: &amp;gt; 1,000,000x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are both approaches side by side, step by step, counting every field multiplication. First, the naive way: for each root of unity, walk all coefficients and compute P(x) = a_0 + a_1*x + a_2*x^2 + ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FftField&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8usize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_root_of_unity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"root exists"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;total_muls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Naive: evaluate P(x) at each root, one by one"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(each * = one field multiplication)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;muls_here&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;x_pow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x_pow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;muls_here&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;x_pow&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;muls_here&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;muls_here&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;total_muls&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;muls_here&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  P(w^{}): {} ({} muls)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="nf"&gt;.repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;muls_here&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;muls_here&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  P(omega^{}) = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Total field multiplications: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_muls&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the same polynomial, same roots, but using the butterfly. Split into even/odd, recurse, combine with + and -. Count the multiplications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FftField&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;ntt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="nf"&gt;.to_vec&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="nf"&gt;.resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;even&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.step_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;odd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.step_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega_sq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;y_even&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ntt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;even&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;omega_sq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;y_odd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ntt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;odd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;omega_sq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;twiddle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;twiddle&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;y_odd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y_even&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y_even&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;twiddle&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8usize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_root_of_unity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"root exists"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ntt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;coeffs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NTT butterfly: all evaluations at once"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(each * = one field multiplication)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;muls_per_depth&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  depth {} (n={:&amp;gt;2}): {} ({} muls)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="nf"&gt;.repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  P(omega^{}) = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Total field multiplications: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same results, different cost. For N = 8 the naive approach uses 127 field multiplications, the butterfly uses 17. That's already 7x fewer, and the ratio keeps growing with N.&lt;/p&gt;

&lt;p&gt;This algorithm is the &lt;strong&gt;Fast Fourier Transform (FFT)&lt;/strong&gt;. When it runs over finite fields instead of complex numbers, it's called the &lt;strong&gt;Number Theoretic Transform (NTT)&lt;/strong&gt;. Same structure, same recursion, different arithmetic.&lt;/p&gt;

&lt;p&gt;One more thing: the recursive halving needs to split cleanly at every level (N -&amp;gt; N/2 -&amp;gt; N/4 -&amp;gt; ... -&amp;gt; 1). That only works if N is a power of two.&lt;/p&gt;

&lt;p&gt;This is why every ZK framework uses domain sizes like 2^16 (65,536) or 2^20 (1,048,576). Not an arbitrary design choice: a direct consequence of the algorithm. When a ZK tutorial says "pad your trace to a power of two," now you know why. We'll talk about traces in the next post.&lt;/p&gt;

&lt;p&gt;The field itself also needs to support this: p - 1 must be divisible by large powers of 2 so the recursive halving has room to work. This is called "2-adicity."&lt;/p&gt;

&lt;p&gt;BN254 has a 2-adicity of 28, meaning it supports NTT domains up to 2^28 (about 268 million points). Plenty for most ZK circuits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's See It Work
&lt;/h2&gt;

&lt;p&gt;Enough theory. Let's use roots of unity for what they're actually for in ZK.&lt;/p&gt;

&lt;p&gt;Remember &lt;a href="https://rustarians.com/polynomials-in-zk-snarks/" rel="noopener noreferrer"&gt;post #1&lt;/a&gt;? We stored data inside a polynomial by choosing x values and evaluating.&lt;/p&gt;

&lt;p&gt;Roots of unity give us those x values for free: omega^0, omega^1, ..., omega^{n-1}. You assign each data value to a root, run the inverse NTT, and out come the polynomial's coefficients. That's interpolation in O(N log N).&lt;/p&gt;

&lt;p&gt;This is what a ZK prover does with the execution trace to get the data in polynomial form (next post).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_poly&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="n"&gt;EvaluationDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Radix2EvaluationDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DenseUVPolynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Polynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;univariate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Build a domain of 4 roots of unity&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Radix2EvaluationDomain&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="nf"&gt;.group_gen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Domain size: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="nf"&gt;.size&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Generator (omega): {:?}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Encode the word "cool" as ASCII values:&lt;/span&gt;
    &lt;span class="c1"&gt;// 'c'=99, 'o'=111, 'o'=111, 'l'=108&lt;/span&gt;
    &lt;span class="c1"&gt;// Each value is assigned to a root of unity:&lt;/span&gt;
    &lt;span class="c1"&gt;//   data[0] at omega^0, data[1] at omega^1, ..., data[3] at omega^3&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;111&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;111&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;108&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Inverse NTT: find the polynomial whose evaluations at roots of unity&lt;/span&gt;
    &lt;span class="c1"&gt;// give back this data. This is interpolation. O(N log N).&lt;/span&gt;
    &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="nf"&gt;.ifft_in_place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// data now holds coefficients. Build the polynomial.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;poly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_coefficients_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify by evaluating at roots of unity, just like post #1.&lt;/span&gt;
    &lt;span class="c1"&gt;// If interpolation worked, P(omega^i) gives back our original data.&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Evaluating the interpolated polynomial:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// omega^0 = 1&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;poly&lt;/span&gt;&lt;span class="nf"&gt;.evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  P(omega^{}) = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;omega&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;That's [99, 111, 111, 108] = &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;cool&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; in ASCII."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The inverse NTT found the right polynomial."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inverse NTT took four ASCII values and found a polynomial that encodes them. We verified it the old-fashioned way: by calling &lt;code&gt;evaluate()&lt;/code&gt; at omega^0, omega^1, omega^2, omega^3 and getting back 99, 111, 111, 108. Same &lt;code&gt;evaluate()&lt;/code&gt; from &lt;a href="https://rustarians.com/polynomials-in-zk-snarks/" rel="noopener noreferrer"&gt;post #1&lt;/a&gt;, but now the x values are roots of unity instead of arbitrary numbers.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.rs/ark-poly/latest/ark_poly/domain/trait.EvaluationDomain.html" rel="noopener noreferrer"&gt;&lt;code&gt;EvaluationDomain&lt;/code&gt; trait&lt;/a&gt; covers more operations beyond FFT/iFFT, but these two are the core.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Just Did
&lt;/h2&gt;

&lt;p&gt;You generated roots of unity over a finite field, saw why their symmetry enables O(N log N) evaluation, and used the inverse NTT to interpolate a polynomial from raw data.&lt;/p&gt;

&lt;p&gt;"Roots of unity"? It is just another name. "Unity" means 1 (the multiplicative identity), "roots" means solutions to an equation. So the whole thing literally means "solutions to z^n = 1."&lt;/p&gt;

&lt;p&gt;Leonhard Euler studied them in 1751 in &lt;a href="https://scholarlycommons.pacific.edu/euler-works/170/" rel="noopener noreferrer"&gt;&lt;em&gt;Recherches sur les racines imaginaires des equations&lt;/em&gt;&lt;/a&gt;. Carl Friedrich Gauss wrote 74 pages formalizing the theory in 1801 in &lt;a href="https://archive.org/details/disquisitionesa00gaus/page/592/mode/2up" rel="noopener noreferrer"&gt;&lt;em&gt;Disquisitiones Arithmeticae&lt;/em&gt;, Section VII&lt;/a&gt;. The name comes from their work. Now you've used the tool behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Up: Execution Trace
&lt;/h2&gt;

&lt;p&gt;We have our fast polynomial engine. Now: how do we represent an actual program, with all its steps and state, as polynomials?&lt;/p&gt;

&lt;p&gt;That's the execution trace. See you there.&lt;/p&gt;

&lt;p&gt;If you want to discuss this with other Rust devs learning ZK, join the &lt;a href="https://rustarians.com/dc" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;What stuck with me the most: when I first saw omega and "rotation" in ZK code, I had no idea what was going on. It felt like geometry, not finite fields.&lt;/p&gt;

&lt;p&gt;In geometry, Greek letters usually name angles, and rotating by angle omega makes perfect sense. In integers modulo a prime? Not so much.&lt;/p&gt;

&lt;p&gt;But then I started looking for roots of unity in other places in mathematics and found them in FFT. And I figured out that (yet again) these are just names. They convey some meaning, but they're also misleading if you take them literally. When you know the origin story, it kinda makes sense why they're called roots of unity even for finite fields.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is post #2 in my "first principles" ZK series for Rust devs. &lt;a href="https://rustarians.com/roots-of-unity/" rel="noopener noreferrer"&gt;Interactive version with runnable code and animations&lt;/a&gt; on rustarians.com.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm running a free live session on March 17: &lt;a href="https://rustarians.com/webinar/" rel="noopener noreferrer"&gt;From Polynomials to Proof in 60 Minutes&lt;/a&gt;. Range check circuit built step by step, with a real use case.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions? Found a bug? &lt;a href="https://www.linkedin.com/in/adam-smolarek/" rel="noopener noreferrer"&gt;linkedin.com/in/adam-smolarek&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cryptography</category>
      <category>zeroknowledge</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Polynomials in ZK SNARKs published: true</title>
      <dc:creator>Adam</dc:creator>
      <pubDate>Tue, 10 Mar 2026 10:27:09 +0000</pubDate>
      <link>https://forem.com/rustarians/polynomials-in-zk-snarkspublished-true-4020</link>
      <guid>https://forem.com/rustarians/polynomials-in-zk-snarkspublished-true-4020</guid>
      <description>&lt;p&gt;&lt;em&gt;This post has interactive code editors on the &lt;a href="https://rustarians.com/polynomials-in-zk-snarks/" rel="noopener noreferrer"&gt;original version&lt;/a&gt; where you can run and modify every Rust snippet in the browser.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Welcome, fellow zk-enjoyer.
&lt;/h2&gt;

&lt;p&gt;You've spent evenings staring at tutorials that don't compile. Explanations that assume you have a PhD in algebra. AI-generated content that looks legit until you run the code.&lt;/p&gt;

&lt;p&gt;I've been in blockchain since 2016. I've written production zk code. This guide teaches polynomials the way I wish someone taught me.&lt;/p&gt;

&lt;p&gt;No PhD required. Code that runs. Let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Expect
&lt;/h2&gt;

&lt;p&gt;Non-formal introductions to zkSNARKs internals. Yes, with scary names: Fast Fourier Transform, KZG commitment scheme, univariate polynomials over finite fields. But without eye-bleeding math.&lt;/p&gt;

&lt;p&gt;I include fancy names so you'll recognize them in whitepapers later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The code&lt;/strong&gt; is written in Rust, edition 2024, using Arkworks libraries. As of January 2026, it compiles and runs.&lt;br&gt;
If something breaks, I'll update it. That's the difference between a real human and a content mill.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Road Map
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Polynomials over finite fields&lt;/strong&gt; &amp;lt;- you are here&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rustarians.com/roots-of-unity/" rel="noopener noreferrer"&gt;Roots of unity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rustarians.com/execution-trace/" rel="noopener noreferrer"&gt;Execution trace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Polynomial interpolation via FFT/NTT&lt;/li&gt;
&lt;li&gt;Applying constraints to polynomials&lt;/li&gt;
&lt;li&gt;Packing into a single polynomial&lt;/li&gt;
&lt;li&gt;Commitment scheme (starting with KZG)&lt;/li&gt;
&lt;li&gt;Proof AND Verification&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eight posts. But you'll understand what happens instead of copy-pasting circuits and praying.&lt;/p&gt;
&lt;h2&gt;
  
  
  Let's go
&lt;/h2&gt;

&lt;p&gt;Remember polynomials from high school? The things nobody explained why they existed?&lt;/p&gt;

&lt;p&gt;Turns out they're the fundamental building block of ZK.&lt;/p&gt;

&lt;p&gt;Polynomials work like a data structure. Think of them as Maps that only accept numbers.&lt;br&gt;
Your data is bits at a low level anyway, so translation is doable.&lt;br&gt;
In addition to storage, polynomials allow you to constrain exactly which numbers go where.&lt;/p&gt;

&lt;p&gt;Today: storing stuff (encoding). Future posts: the constraints magic (business logic).&lt;/p&gt;
&lt;h2&gt;
  
  
  Your First Polynomial
&lt;/h2&gt;

&lt;p&gt;Here's a polynomial that encodes a message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2x^3 - 15x^2 + 28x + 96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you did not know how it was created, it looks like regular equation. But plug in x values 3, 1, 5, 2, and you get the message back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_poly&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;DenseUVPolynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Polynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;univariate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Neg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;BigInteger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PrimeField&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ScalarField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// P(x) = 2x^3 - 15x^2 + 28x + 96 mod Fr_curve::MODULUS&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ScalarField&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_coefficients_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.neg&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;evaluation1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="nf"&gt;.evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;evaluation2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="nf"&gt;.evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;evaluation3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="nf"&gt;.evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;evaluation4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="nf"&gt;.evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"polynomial evaluations (x,y) = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;evaluation1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;evaluation2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;evaluation3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;evaluation4&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluation1&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_bytes_le&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluation2&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_bytes_le&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;c3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluation3&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_bytes_le&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;c4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluation4&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_bytes_le&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"polynomial evaluations y as letters = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;c3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;c4&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"how polynomial actually looks like: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You use specific x values (3, 1, 5, 2) as indices to encode the data.&lt;br&gt;
Later, we'll encode business logic (constraints) as polynomials too.&lt;/p&gt;

&lt;p&gt;Ok, cool we have it saved something in form of a polynomial but WHY?&lt;/p&gt;

&lt;p&gt;Because there is this thing called &lt;a href="https://en.wikipedia.org/wiki/Schwartz%E2%80%93Zippel_lemma" rel="noopener noreferrer"&gt;Schwartz-Zippel lemma&lt;/a&gt;.&lt;br&gt;
Do not be afraid young padawan, another fancy name, but tool very useful it is.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one and only, the OG: Schwartz-Zippel lemma
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Schwartz%E2%80%93Zippel_lemma" rel="noopener noreferrer"&gt;Schwartz-Zippel&lt;/a&gt;&lt;/strong&gt; simplified:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two different polynomials will almost never give the same output for a random input.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you know hash functions, this is similar. Different inputs produce different outputs. With hashes, the input is data. With Schwartz-Zippel the input is a polynomial plus a random value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does this matter?&lt;/strong&gt; You can verify someone has a specific polynomial by asking for ONE evaluation at a random point. One number checks an entire polynomial. Insanely efficient.&lt;/p&gt;

&lt;p&gt;Below: two different polynomials evaluated at a random point. Run it a few times and see that evaluations are always different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_poly&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;DenseUVPolynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Polynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;univariate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_std&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="n"&gt;UniformRand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;SeedableRng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StdRng&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;SystemTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UNIX_EPOCH&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ScalarField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;polynomial1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ScalarField&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_coefficients_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;polynomial2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ScalarField&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_coefficients_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;since_the_epoch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SystemTime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.duration_since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UNIX_EPOCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"I am expecting that some time has passed since EPOCH"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StdRng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SeedableRng&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;seed_from_u64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since_the_epoch&lt;/span&gt;&lt;span class="nf"&gt;.as_secs&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;evaluation1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;polynomial1&lt;/span&gt;&lt;span class="nf"&gt;.evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;evaluation2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;polynomial2&lt;/span&gt;&lt;span class="nf"&gt;.evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"polynomial1 evaluation (x,y) = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evaluation1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"polynomial2 evaluation (x,y) = {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evaluation2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nd"&gt;assert_ne!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evaluation1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evaluation2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assertion should never fail. Collision probability is less than 1 in 10^76. To give you an idea, the observable universe has 10^80 atoms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back to "cool" example.&lt;/strong&gt; You can verify I have the same polynomial by picking random x (say, 42), asking me to evaluate at that point, and comparing results. Same value = same polynomial (with overwhelming probability).&lt;/p&gt;

&lt;p&gt;This extends to constraints and business logic. But that's for future posts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Polynomials + Schwartz-Zippel = encode stuff and verify with a single number.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Wait, Why Does -15 Look So Weird?"
&lt;/h2&gt;

&lt;p&gt;When you print the polynomial, -15 shows up as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;21888242871839275222246405745257275088548364400416034343698204186575808495602
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the hell?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_poly&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;DenseUVPolynomial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;univariate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Neg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DensePolynomial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;DensePolynomial&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_coefficients_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nn"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.neg&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nn"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"how polynomial should look like: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;96 + &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;28 * x - &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;15 * x^2 + &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;2 * x^3"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"how polynomial actually looks like: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;polynomial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That giant number IS -15. BUT in modular arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example on smaller numbers first.&lt;/strong&gt; Regular numbers go to infinity both ways. Modular arithmetic gives you finite numbers. With mod 29, you have 29 numbers. To get infinity working with a finite number of elements they form a circle (ring). After 28 comes 0, then 1, 2, 3... Behind 0 comes 28, then 27...&lt;/p&gt;

&lt;p&gt;So with mod 29, what acts like -15? You need something that adds to 15 and gives 0. That's 14. Because 15 + 14 = 29. And there is no 29, after 28 you wrap back to 0. So 15 + 14 = 0 mod 29. So 14 "acts like" -15 in this case.&lt;/p&gt;

&lt;p&gt;Let's check how -15 looks like with the big modulus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;BigInteger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PrimeField&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Neg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ScalarField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;negative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.neg&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"negative + positive in modular arithmetic: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;negative&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;negative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;negative&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;positive&lt;/span&gt;&lt;span class="nf"&gt;.add_with_carry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;negative&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"negative + positive in regular arithmetic: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ScalarField::MODULUS: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MODULUS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Same trick works for multiplication.&lt;/strong&gt; Normally, if you multiply a number by its inverse you get 1. With regular numbers, inverse of 5 is 1/5. Multiply them: 5 x 1/5 = 1.&lt;/p&gt;

&lt;p&gt;But with modular arithmetic? No such thing as a fraction (1/5). Instead, there's some integer that "acts like" 1/5. Finding it is much harder, but we can do it with extended Euclidean algorithm (another fancy name).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_bn254&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fr&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ark_ff&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ScalarField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Bn254Fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;five&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;five_inverse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;five&lt;/span&gt;&lt;span class="nf"&gt;.inverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"5 inverse: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;five_inverse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"5 * 5_inverse: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;five&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;five_inverse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why do we care?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Addition/subtraction and multiplication/division work a bit differently. Instead of regular numbers we have numbers modulo some prime.&lt;/p&gt;

&lt;p&gt;Because we have all those things we can use another fancy name: Finite Field. Just a name you'll see in white papers. Now you know what it means.&lt;/p&gt;

&lt;p&gt;We can call it many other names, like Knight of the Square Table, or Humpty Dumpty, but someone was faster and already called it Finite Field, and it kinda stuck. The guy was Evariste Galois.&lt;/p&gt;

&lt;p&gt;Those Finite Fields have a critical property. Multiplication and powers are easy to compute. Going backwards (logarithms) is stupidly hard. ZK security depends on this asymmetry. Post #7 (KZG commitment scheme) shows exactly how.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Just Did
&lt;/h2&gt;

&lt;p&gt;You computed evaluation of univariate polynomials over a finite field. Specifically, the scalar field of the elliptic curve BN254.&lt;/p&gt;

&lt;p&gt;Told you at the beginning: these are names. They can't hurt you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Up: Roots of Unity
&lt;/h2&gt;

&lt;p&gt;How do we efficiently put data INTO polynomials? By using something called "roots of unity."&lt;/p&gt;

&lt;p&gt;It's a bunch of numbers with useful properties. The name sounds intimidating. It isn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rustarians.com/roots-of-unity/" rel="noopener noreferrer"&gt;See you there.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is post #1 in my "first principles" ZK series for Rust devs. &lt;a href="https://rustarians.com/polynomials-in-zk-snarks/" rel="noopener noreferrer"&gt;Interactive version with runnable code&lt;/a&gt; on rustarians.com.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm running a free live session on March 17: &lt;a href="https://rustarians.com/webinar/" rel="noopener noreferrer"&gt;From Polynomials to Proof in 60 Minutes&lt;/a&gt;. Range check circuit built step by step, with a real use case.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions? Found a bug? &lt;a href="https://www.linkedin.com/in/adam-smolarek/" rel="noopener noreferrer"&gt;linkedin.com/in/adam-smolarek&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cryptography</category>
      <category>zeroknowledge</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
