<?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: 0xluk3</title>
    <description>The latest articles on Forem by 0xluk3 (@0xluk3).</description>
    <link>https://forem.com/0xluk3</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%2F3863419%2F44472f1f-a06f-4d96-a5e0-e86d19ce83ae.png</url>
      <title>Forem: 0xluk3</title>
      <link>https://forem.com/0xluk3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/0xluk3"/>
    <language>en</language>
    <item>
      <title>Hello Noir! [Part 1]</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Tue, 07 Apr 2026 07:28:57 +0000</pubDate>
      <link>https://forem.com/0xluk3/hello-noir-part-1-4m18</link>
      <guid>https://forem.com/0xluk3/hello-noir-part-1-4m18</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;ZK is a hot topic. But what does it even mean to build a ZK circuit? Let's build a barebone, super basic circuit so you can have a better understanding what its part of.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. What we're building and the toolchain
&lt;/h2&gt;

&lt;p&gt;What we're building through this series is a SNARK - a Succinct Non-interactive Argument of Knowledge. Barretenberg, the proving backend we'll use, implements UltraHonk - a PLONK-based proof system. PLONK and its descendants are SNARKs. The zero-knowledge part is actually optional (Barretenberg has a &lt;code&gt;--zk&lt;/code&gt; flag for that), so what we produce is strictly a SNARK, not necessarily a zkSNARK - but the ecosystem loosely calls everything "zk" since the tooling supports it.&lt;br&gt;
Here's the high-level flow of what we're doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We, the &lt;em&gt;prover&lt;/em&gt; (the user), want to prove something - some statement, like "I am more than 20 years old"&lt;/li&gt;
&lt;li&gt;We do this by supplying evidence that backs our statement - a blob of bytes, mathematically encoding our proof in a privacy-friendly way&lt;/li&gt;
&lt;li&gt;Another party, the &lt;em&gt;verifier&lt;/em&gt;, checks our proof according to some math formula&lt;/li&gt;
&lt;li&gt;Since the verifier can live on-chain, it can be queried for the result and act upon it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we use these tools to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://noir-lang.org/" rel="noopener noreferrer"&gt;Noir&lt;/a&gt; - write the circuit (the constraints)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://barretenberg.aztec.network/docs/getting_started/" rel="noopener noreferrer"&gt;Barretenberg&lt;/a&gt; - generate proofs and verifier contracts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://book.getfoundry.sh/" rel="noopener noreferrer"&gt;Foundry&lt;/a&gt; - deploy and test on-chain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, why those? We should be doing Noir ZK right?&lt;/p&gt;

&lt;p&gt;Yes, but Noir is only the language of constraints - it tells the system &lt;em&gt;what&lt;/em&gt; to prove. We also need to generate a proof (this will be what users submit) and a component to check if it's verifiable. Barretenberg is the proving backend that takes the compiled circuit and your inputs, produces the actual cryptographic proof, and can also generate a Solidity verifier contract. Foundry handles the on-chain side - deploying and testing that verifier. We won't use Foundry in this post, but it shows up in Part 2.&lt;/p&gt;

&lt;p&gt;This is a barebone implementation of a ZK app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7b0a9o1145ae2p8ypqy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7b0a9o1145ae2p8ypqy.png" alt="Noir Toolchain Overview"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Setting up the environment
&lt;/h2&gt;

&lt;p&gt;Noir's toolchain depends on &lt;a href="https://www.rust-lang.org/tools/install" rel="noopener noreferrer"&gt;Rust&lt;/a&gt;. If you don't have it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://sh.rustup.rs | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install &lt;a href="https://noir-lang.org/docs/getting_started/noir_installation" rel="noopener noreferrer"&gt;Noir via &lt;code&gt;noirup&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
noirup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify with &lt;code&gt;nargo --version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Install &lt;a href="https://barretenberg.aztec.network/docs/getting_started/" rel="noopener noreferrer"&gt;Barretenberg via &lt;code&gt;bbup&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/master/barretenberg/bbup/install | bash
bbup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify with &lt;code&gt;bb --version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's it. You're ready to write a circuit.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Writing a sample circuit
&lt;/h2&gt;

&lt;p&gt;Let's try to write a sample circuit. This will be a super dummy, in fact meaningless thing from ZK standpoint - just to understand the process and see how things work. We will build something actually working in the next article.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nargo new hello_world
&lt;span class="nb"&gt;cd &lt;/span&gt;hello_world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project successfully created! It is located at /home/dev/noir-hello-world/hello_world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;Nargo.toml&lt;/code&gt; (the project manifest, think &lt;code&gt;package.json&lt;/code&gt; or &lt;code&gt;Cargo.toml&lt;/code&gt;) and &lt;code&gt;src/main.nr&lt;/code&gt; - your circuit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[package]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hello_world"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"bin"&lt;/span&gt;
&lt;span class="py"&gt;authors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the default &lt;code&gt;src/main.nr&lt;/code&gt;:&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="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assert&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="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;main&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Uncomment to make test fail&lt;/span&gt;
    &lt;span class="c1"&gt;// main(1, 1);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our circuit just checks if &lt;code&gt;x&lt;/code&gt; is not equal to &lt;code&gt;y&lt;/code&gt;. Simple. But notice the types - &lt;code&gt;x&lt;/code&gt; is &lt;code&gt;u64&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; is &lt;code&gt;pub u64&lt;/code&gt;. In Noir, every input is private by default. If you want an input to be visible to the verifier (and to the world), you mark it &lt;code&gt;pub&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So here, &lt;code&gt;x&lt;/code&gt; is private and &lt;code&gt;y&lt;/code&gt; is public. When a proof is generated, the verifier can see &lt;code&gt;y&lt;/code&gt; but learns nothing about &lt;code&gt;x&lt;/code&gt;. The proof only guarantees that &lt;em&gt;some&lt;/em&gt; value of &lt;code&gt;x&lt;/code&gt; exists that satisfies the constraint.&lt;/p&gt;

&lt;p&gt;Here's a more intuitive example - imagine an age verification circuit:&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="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;min_age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;min_age&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;Here &lt;code&gt;min_age&lt;/code&gt; is explicitly set to public. While we generate the proof, &lt;code&gt;age&lt;/code&gt; is not revealed, while &lt;code&gt;min_age&lt;/code&gt; is public - anyone can see you're checking against 18. The proof says "this person is old enough" without revealing whether they're 19 or 90.&lt;/p&gt;

&lt;p&gt;Now, we can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nargo check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This validates your circuit and creates a &lt;code&gt;Prover.toml&lt;/code&gt; file - a template for your inputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="py"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's input some values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;span class="py"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nargo execute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[hello_world] Circuit witness successfully solved
[hello_world] Witness saved to target/hello_world.gz
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The circuit compiled, the inputs satisfied the constraint (&lt;code&gt;2 != 1&lt;/code&gt;), and &lt;code&gt;nargo&lt;/code&gt; saved the result.&lt;/p&gt;

&lt;p&gt;And what if we tried to prove something that does not meet constraints? Change &lt;code&gt;Prover.toml&lt;/code&gt; so both values are equal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;span class="py"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;nargo execute&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: Failed constraint
  ┌─ /home/dev/noir-hello-world/hello_world/src/main.nr:2:12
  │
2 │     assert(x != y);
  │            ------
  │
  = Call stack:
    1. /home/dev/noir-hello-world/hello_world/src/main.nr:2:12

Failed to solve program: 'Cannot satisfy constraint'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inputs don't satisfy the rules. No witness is generated, no proof to produce.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. What did nargo just produce
&lt;/h2&gt;

&lt;p&gt;Look in the &lt;code&gt;target/&lt;/code&gt; directory. You'll find two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hello_world.json&lt;/code&gt;&lt;/strong&gt; - the compiled circuit (ACIR) in a structured format. It contains the constraints and instructions that define your program after compilation. This is generated by &lt;code&gt;nargo compile&lt;/code&gt; and doesn't depend on your specific input values - the same circuit can be used with different inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hello_world.gz&lt;/code&gt;&lt;/strong&gt; - the witness. This contains the specific values (both public and private) that satisfy the constraints. This is generated by &lt;code&gt;nargo execute&lt;/code&gt; and &lt;em&gt;does&lt;/em&gt; depend on what you put in &lt;code&gt;Prover.toml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are &lt;strong&gt;two different artifacts&lt;/strong&gt;, not compressed and uncompressed versions of the same thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosr0upr4yhy1a656v0uv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosr0upr4yhy1a656v0uv.png" alt="Compile and Execute Flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To generate an actual cryptographic proof, you need &lt;em&gt;both&lt;/em&gt;: the circuit (what to prove) and the witness (the values that satisfy it). That's where Barretenberg comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. What's next
&lt;/h2&gt;

&lt;p&gt;So we have constraints (not equality requirement) and a proof that we were able to achieve that. But what &lt;code&gt;nargo execute&lt;/code&gt; produced is not a cryptographic proof - it's just a confirmation that your inputs work. A verifier contract can't do anything with a &lt;code&gt;.gz&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;I don't want to make articles too long. Because people tend to be scared and walk away when they see the scrollbar - so I moved that part to Part 2. In Part 2, we'll use Barretenberg to take the compiled circuit and witness, generate an actual cryptographic proof, verify it locally, and generate a Solidity verifier contract for on-chain verification.&lt;/p&gt;

&lt;p&gt;Part 2 coming soon&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://zkshark.notion.site/Hot-Chocolate-Beginners-guide-14907561ca1a80e68bd1d9245a53fd95" rel="noopener noreferrer"&gt;Hot Chocolate&lt;/a&gt; - beginner's guide to Noir&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://noir-lang.org/docs/tutorials/noirjs_app" rel="noopener noreferrer"&gt;NoirJS app tutorial&lt;/a&gt; - official Noir docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vkpatva/noir" rel="noopener noreferrer"&gt;vkpatva/noir&lt;/a&gt; - example Noir circuits repo&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://barretenberg.aztec.network/docs/getting_started/" rel="noopener noreferrer"&gt;Barretenberg&lt;/a&gt; - Aztec guide to Barretenberg&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>noir</category>
      <category>zeroknowledge</category>
      <category>blockchain</category>
      <category>security</category>
    </item>
    <item>
      <title>Math cheatsheet before you deep-dive into ZK</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Mon, 06 Apr 2026 15:26:59 +0000</pubDate>
      <link>https://forem.com/0xluk3/math-cheatsheet-before-you-deep-dive-into-zk-1bo6</link>
      <guid>https://forem.com/0xluk3/math-cheatsheet-before-you-deep-dive-into-zk-1bo6</guid>
      <description>&lt;p&gt;We're not going to explain ZK in five minutes here. We're not even going to touch it. But there are a handful of math topics you &lt;strong&gt;have to&lt;/strong&gt; understand before anything in this series makes sense.&lt;/p&gt;

&lt;p&gt;Are you scared of those nasty math symbols? Does opening an arxiv paper make you want to hide under your bed? Good. Same here. So let's get comfortable with it slowly, one concept at a time, with zero greek letters and some things you can click on.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Modular arithmetic
&lt;/h2&gt;

&lt;p&gt;Think of a clock. A normal clock has 12 positions (0 through 11 if you're a programmer). If it's 10 o'clock and you add 5 hours, you don't get 15. You get 3. The number wraps around.&lt;/p&gt;

&lt;p&gt;That's modular arithmetic. The &lt;code&gt;mod&lt;/code&gt; operator gives you the remainder after division:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 + 5 = 15 → 15 mod 12 = 3
7 + 7 = 14 → 14 mod 12 = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In crypto, the modulus isn't 12. It's a prime number so large it has 77 digits. But the principle is identical: every result wraps back into a fixed range, and that wrapping destroys information about the original inputs.&lt;/p&gt;

&lt;p&gt;Try it yourself. The clock below uses a small modulus so you can watch values wrap around:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://luk3.tech/blog/math-cheatsheet-before-zk" rel="noopener noreferrer"&gt;Interactive: Modular arithmetic clock visualization — try it on luk3.tech&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Formal explanation: &lt;a href="https://en.wikipedia.org/wiki/Modular_arithmetic" rel="noopener noreferrer"&gt;Modular arithmetic on Wikipedia&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Finite fields
&lt;/h2&gt;

&lt;p&gt;A finite field (also called a Galois field) is a set of numbers where you can add, subtract, multiply, and divide, and you never leave the set. Every operation wraps back into the same pool of values.&lt;/p&gt;

&lt;p&gt;More precisely, a finite field &lt;strong&gt;GF(p)&lt;/strong&gt; is the set of integers &lt;code&gt;{0, 1, 2, ..., p-1}&lt;/code&gt; where &lt;code&gt;p&lt;/code&gt; is prime, and all arithmetic is done mod &lt;code&gt;p&lt;/code&gt;. Three properties matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Closed&lt;/strong&gt;: No operation produces a result outside the set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every element has an inverse&lt;/strong&gt;: For any number &lt;code&gt;a&lt;/code&gt; in the field, there exists some &lt;code&gt;b&lt;/code&gt; such that &lt;code&gt;a * b = 1 (mod p)&lt;/code&gt;. Division always works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No escape&lt;/strong&gt;: You can chain as many operations as you want. You're still in the field.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why does crypto care? Because finite fields let you do complex algebra on huge numbers while guaranteeing that every intermediate result stays within a fixed, predictable range. No overflow, no floating point issues, no surprises. And the modular wrapping makes it extremely hard to reverse-engineer what inputs produced a given output.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Formal explanation: &lt;a href="https://en.wikipedia.org/wiki/Finite_field" rel="noopener noreferrer"&gt;Finite field on Wikipedia&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The discrete logarithm problem
&lt;/h2&gt;

&lt;p&gt;Here's the core asymmetry that makes public-key cryptography possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The easy direction&lt;/strong&gt;: Given a base &lt;code&gt;g&lt;/code&gt;, an exponent &lt;code&gt;k&lt;/code&gt;, and a modulus &lt;code&gt;p&lt;/code&gt;, computing &lt;code&gt;g^k mod p&lt;/code&gt; is fast. Computers are good at exponentiation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;g = 3, k = 5, p = 7
3^5 = 243 → 243 mod 7 = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The hard direction&lt;/strong&gt;: Given &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, and the result &lt;code&gt;5&lt;/code&gt;, find &lt;code&gt;k&lt;/code&gt;. With small numbers you can just try them all. With numbers that have 77 digits? No known algorithm can do it efficiently. You'd be guessing until the heat death of the universe.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;discrete logarithm problem (DLP)&lt;/strong&gt;. "Discrete" because you're working in a finite field (discrete values, not continuous). "Logarithm" because you're trying to find the exponent.&lt;/p&gt;

&lt;p&gt;When this same problem is framed on an elliptic curve (finding how many times a point was "multiplied" to produce another point), it's called the &lt;strong&gt;Elliptic Curve Discrete Logarithm Problem (ECDLP)&lt;/strong&gt;. Same idea, different algebraic structure, even harder to break.&lt;/p&gt;

&lt;p&gt;Important distinction: "hard" here means &lt;em&gt;computationally infeasible&lt;/em&gt;, not mathematically impossible. The answer exists. Nobody forbids you from finding it. It's just that the fastest known algorithms would take longer than the age of the universe on current hardware.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Formal explanation: &lt;a href="https://en.wikipedia.org/wiki/Discrete_logarithm" rel="noopener noreferrer"&gt;Discrete logarithm on Wikipedia&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Hash functions
&lt;/h2&gt;

&lt;p&gt;A hash function takes any input (a single character, a novel, a video file) and produces a fixed-size output. SHA-256 always gives you 256 bits (64 hex characters). SHA-3 gives you the same. Doesn't matter if your input is four letters or four gigabytes.&lt;/p&gt;

&lt;p&gt;But how? How does the word &lt;code&gt;math&lt;/code&gt; turn into &lt;code&gt;a0885e289f3e77a14e06e6887a1fc93b5ed2e14cfe7f7052805fe92e1a0e0e38&lt;/code&gt;?&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually happens inside a hash
&lt;/h3&gt;

&lt;p&gt;Let's walk through the rough steps. Different algorithms (SHA-2, SHA-3, BLAKE) vary in the details, but the skeleton is the same:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Convert to binary.&lt;/strong&gt; Your input becomes raw bytes. The string &lt;code&gt;math&lt;/code&gt; becomes four ASCII values: &lt;code&gt;109 97 116 104&lt;/code&gt;, which in binary is &lt;code&gt;01101101 01100001 01110100 01101000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Pad the message.&lt;/strong&gt; The algorithm needs the input to be a specific length (a multiple of the block size). So it appends a &lt;code&gt;1&lt;/code&gt; bit, then enough &lt;code&gt;0&lt;/code&gt; bits, then the original message length. Now you have a neat, fixed-size block to work with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Initialize state.&lt;/strong&gt; The algorithm starts with a set of fixed constants as its internal state. These aren't secret. They're defined in &lt;a href="https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf" rel="noopener noreferrer"&gt;the spec&lt;/a&gt; (for SHA-256 they come from the fractional parts of the square roots of the first eight primes, because why not).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Compress, round after round.&lt;/strong&gt; This is where the magic happens. The padded message gets split into chunks, and each chunk gets fed through a &lt;strong&gt;compression function&lt;/strong&gt; that mixes it into the internal state. SHA-256 runs 64 rounds. SHA-3 runs 24. Each round does a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bitwise operations&lt;/strong&gt;: XOR, AND, NOT, rotations. These scramble the bits in non-linear ways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular addition&lt;/strong&gt;: Adding values mod 2^32, which causes carries to cascade unpredictably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing&lt;/strong&gt;: Bits from different positions influence each other, so information spreads across the entire state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The visualization below walks through the full SHA-256 computation for the word "math", step by step. You can see the padding, the message schedule, and how each round scrambles the working variables until the final hash emerges.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://luk3.tech/blog/math-cheatsheet-before-zk" rel="noopener noreferrer"&gt;Interactive: SHA-256 step-by-step computation walkthrough — try it on luk3.tech&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Output.&lt;/strong&gt; The final internal state (or a portion of it) becomes your hash.&lt;/p&gt;

&lt;p&gt;The result is deterministic (same input, same output, every time) but practically irreversible. You can't reconstruct &lt;code&gt;math&lt;/code&gt; from the hash any more than you can reconstruct an egg from an omelette. This makes hash functions a kind of &lt;strong&gt;trapdoor&lt;/strong&gt;: easy to compute forward, impossible to reverse. You'll see this same pattern everywhere in cryptography. Your public key is derived from your private key through a trapdoor (the discrete log). Digital signatures use one. Hash functions are the simplest example: one-way, no way back.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Formal explanation: &lt;a href="https://en.wikipedia.org/wiki/Cryptographic_hash_function" rel="noopener noreferrer"&gt;Cryptographic hash function on Wikipedia&lt;/a&gt; · &lt;a href="https://en.wikipedia.org/wiki/Trapdoor_function" rel="noopener noreferrer"&gt;Trapdoor function on Wikipedia&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Constraints: how ZK thinks about computation
&lt;/h2&gt;

&lt;p&gt;Here's where things start to feel unfamiliar.&lt;/p&gt;

&lt;p&gt;Normally, when you want to prove you ran a computation correctly, you just re-run it and check the answer. ZK proofs take a completely different approach. Instead of re-running the program, you express the entire computation as a &lt;strong&gt;set of equations&lt;/strong&gt; (called constraints) that must all be satisfied simultaneously.&lt;/p&gt;

&lt;p&gt;A simple example. Say you want to prove you know two numbers that multiply to 35. Instead of revealing "5 and 7", you write a constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a * b = 35
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you can provide values for &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; that satisfy this equation, you've proven you know them. The constraint system doesn't care &lt;em&gt;how&lt;/em&gt; you found the values. It only cares that they satisfy the equations.&lt;/p&gt;

&lt;p&gt;Real ZK systems express entire programs this way. A program that checks a password, verifies a merkle proof, or validates a transaction gets compiled into thousands (or millions) of constraints. The most common format for this is called &lt;strong&gt;R1CS&lt;/strong&gt; (Rank-1 Constraint System), where every constraint has the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(left) * (right) = (output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, and &lt;code&gt;output&lt;/code&gt; is a linear combination of variables. The entire program becomes a system of these equations.&lt;/p&gt;

&lt;p&gt;The key insight: "I know values that satisfy all these constraints" is a statement you can prove without revealing the values themselves.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Formal explanation: &lt;a href="https://en.wikipedia.org/wiki/Rank-1_constraint_system" rel="noopener noreferrer"&gt;R1CS on Wikipedia&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The idea behind zero-knowledge proofs
&lt;/h2&gt;

&lt;p&gt;Now you have all the pieces. ZK proofs combine the concepts above into a single protocol.&lt;/p&gt;

&lt;p&gt;The setup: there's a &lt;strong&gt;prover&lt;/strong&gt; (who knows a secret) and a &lt;strong&gt;verifier&lt;/strong&gt; (who wants to be convinced the prover knows it, without learning the secret).&lt;/p&gt;

&lt;p&gt;A zero-knowledge proof has three properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Completeness&lt;/strong&gt;: If the prover actually knows the secret, they can always convince the verifier. Honest provers always succeed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soundness&lt;/strong&gt;: If the prover doesn't know the secret, they can't trick the verifier (except with negligible probability). Liars get caught.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-knowledge&lt;/strong&gt;: The verifier learns nothing beyond the fact that the statement is true. No information about the secret leaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put those together with constraint systems and you get the full picture: the prover takes a program, compiles it into constraints, plugs in their secret values, and generates a proof that all constraints are satisfied. The verifier checks the proof without ever seeing the secret values.&lt;/p&gt;

&lt;p&gt;The math that makes this actually work (polynomial commitments, elliptic curve pairings, Fiat-Shamir transforms) is deep. Future posts will go there. For now, the mental model is enough: constraints define what "correct" means, and ZK proofs let you demonstrate correctness without revealing your inputs.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Formal explanation: &lt;a href="https://en.wikipedia.org/wiki/Zero-knowledge_proof" rel="noopener noreferrer"&gt;Zero-knowledge proof on Wikipedia&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>math</category>
      <category>blockchain</category>
      <category>security</category>
    </item>
  </channel>
</rss>
