<?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>DEV Community: 0xluk3</title>
    <description>The latest articles on DEV Community by 0xluk3 (@0xluk3).</description>
    <link>https://dev.to/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>DEV Community: 0xluk3</title>
      <link>https://dev.to/0xluk3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0xluk3"/>
    <language>en</language>
    <item>
      <title>Web3 Security and OPSEC Checklist: Beyond the Smart Contract Audit</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Sun, 19 Apr 2026 14:10:50 +0000</pubDate>
      <link>https://dev.to/0xluk3/web3-security-and-opsec-checklist-beyond-the-smart-contract-audit-4hb8</link>
      <guid>https://dev.to/0xluk3/web3-security-and-opsec-checklist-beyond-the-smart-contract-audit-4hb8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;RandomExampleFinance has been audited. Smart contracts - clean. And yet, six months later, funds are gone. This post is about everything the audit didn't cover.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This can happen for many reasons, not only because of hackers befriending you and hacking, or lost multisig. If you want to be able to perform similar exercise to your own company, follow along.&lt;/p&gt;




&lt;h2&gt;
  
  
  The overview
&lt;/h2&gt;

&lt;p&gt;Let's assume we have a company RandomExampleFinance (I hope there is no real company under that name). This is a small sized web3 startup. We have a CEO, a CTO, two marketing persons, 5 devs reporting to CTO and a HR employee. The company works remotely - everyone is a digital nomad, the infra is shared online. The device policy is purely &lt;code&gt;BYOD&lt;/code&gt; and there is no expensive corporate &lt;code&gt;EDR&lt;/code&gt; connected to a 24/7 &lt;code&gt;SOC&lt;/code&gt; - sorry, we're on budget! Company's main product is a DeFi that allows some novel way to earn yield - it doesn't really matter how.&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%2Fp4yefnck5otzb36jiyl1.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%2Fp4yefnck5otzb36jiyl1.png" alt="Overview of attack vectors" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The assets
&lt;/h2&gt;

&lt;p&gt;In order to understand the threat model, we have to first define, what we are protecting. A holiday photos that an employee keeps on his laptop? No, we should care about the core thing: Private key to our funds and those funds. And in second order: anything that could lead to losing them: application that users use, trust to your website domain, your social media accounts. This is the core we need to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the funds could be stolen?
&lt;/h2&gt;

&lt;p&gt;There are few common scenarios of attacks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A smart contract exploit leading to loss of funds&lt;/li&gt;
&lt;li&gt;A web2 type exploit leading to loss of funds (no user interaction)&lt;/li&gt;
&lt;li&gt;A web2 type exploit leading to loss of funds (user interaction)&lt;/li&gt;
&lt;li&gt;Exploit or attack against exact person that has access to the funds (keys)&lt;/li&gt;
&lt;li&gt;Dependency confusion / Supply chain attack&lt;/li&gt;
&lt;li&gt;Governance/multisig or malicious employee attack&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of them has its different reasons and different way to prevent it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. A smart contract exploit leading to loss of funds
&lt;/h2&gt;

&lt;p&gt;This is probably the easiest one to understand and most known in the web3 industry. Simply said, the smart contract may have a logic/coding flaw that allows to steal the funds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation:&lt;/strong&gt; smart contract audit, running a bug bounty program to support ongoing testing - one-time test doesn't guarantee anything, since it represents a point in time - but the techniques evolve, and what if a new type of vulnerability is discovered right after the audit? Use the approach described below to maximize audit efficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Companies tend to believe that they may change just one line of code vs audited commit and it shouldn't change anything. But this often turns out to be not true. It is always better to reach out to whoever audited and ask (even if for extra cost) to verify the addition.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A web2 type exploit leading to loss of funds (no user interaction)
&lt;/h2&gt;

&lt;p&gt;This one may happen only in some cases. No user interaction means that some software running on the servers of your app can be exploited remotely.&lt;/p&gt;

&lt;p&gt;A web2 exploit is something &lt;a href="https://owasp.org/www-project-web-security-testing-guide/" rel="noopener noreferrer"&gt;OWASP WSTG&lt;/a&gt; describes. You probably heard of &lt;code&gt;SQL Injection&lt;/code&gt; or &lt;code&gt;Remote Code Execution&lt;/code&gt;. So the OWASP testing guide has 400+ pages and there are multiple exploit chains possible.&lt;/p&gt;

&lt;p&gt;Even if the dApp is only a frontend, it can be abused - but this will be covered in the later point.&lt;/p&gt;

&lt;p&gt;This depends on your attack surface - here are the key questions to ask:&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%2F2jjl7rdplcui3b5s69f0.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%2F2jjl7rdplcui3b5s69f0.png" alt="Example Web2 Attack Vectors" width="693" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation:&lt;/strong&gt; Asset inventory first - know every subdomain, server, and cloud resource you own. Put everything customer-facing behind Cloudflare. Anything not meant for clients shouldn't be internet-accessible at all. Commission a blackbox OSINT sweep before a whitebox pentest - you may be surprised what's findable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Scope your pentest around your actual assets, not your most popular app. "Test everything" with no asset inventory just burns budget on the wrong things.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A web2 type exploit leading to loss of funds (user interaction)
&lt;/h2&gt;

&lt;p&gt;User interaction means that for the attack to succeed, users need to do something - for instance, approve a malicious transaction. However, from experience with web3 hacks - it isn't always difficult to convince users, so the "user interaction" part should never be underestimated.&lt;/p&gt;

&lt;p&gt;Example of such attack may be: a site that serves some 3rd party data e.g. token informations, suddenly is hacked and the tokens start to distribute malicious scripts. Or a 3rd party provider is compromised and scripts on your website are hijacked. What can you do?&lt;/p&gt;

&lt;p&gt;Typical technical controls to help it are well-known frontend hardening measures such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity" rel="noopener noreferrer"&gt;SRI (Subresource Integrity)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" rel="noopener noreferrer"&gt;CSP (Content Security Policy)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security" rel="noopener noreferrer"&gt;Cookie flags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;CORS headers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them secure the user browser instructing it on what to load. This way, anything injected, like scripts, won't be effective. And the truth is, that every penetration test flags those - typically as "low" findings. Every free scanner finds those. But the impact of having or not having those is huge, because it either blocks any unwanted scripts or not - and despite "official" &lt;code&gt;CVSS&lt;/code&gt; score for those, in web3 injections of scripts lead to great damage, hence those controls should be prioritized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation:&lt;/strong&gt; SRI, CSP, proper cookie flags. Don't pull latest dependencies immediately on release. Treat third-party scripts as untrusted by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; "Your frontend is only as secure as the least secure third-party script it loads." Since those are typically low findings, rarely someone patches it (because configuring CSP is painful).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Exploit or attack against exact person that has access to the funds (keys)
&lt;/h2&gt;

&lt;p&gt;Knowing what happened to Drift protocol, one can ask if it is even possible to secure against this style of attacks. Building a 6 months relationship has no precedence in any red team and in most cases 99% of people in that situation would open whatever is sent from a long term work colleague. And what is the way from opening malicious file to losing the key?&lt;/p&gt;

&lt;p&gt;Typically when an automated &lt;a href="https://en.wikipedia.org/wiki/Remote_access_trojan" rel="noopener noreferrer"&gt;RAT&lt;/a&gt; runs on a workstation, it has a stealer module in it. Which means it is an automated plunderer who knows where to look and what to look for. It takes whatever it finds and disappears. In most cases these are built to evade &lt;code&gt;AV&lt;/code&gt; detections, too.&lt;/p&gt;

&lt;p&gt;Also think that the phishing may not always be a &lt;code&gt;RAT&lt;/code&gt; execution. It may be AWS account takeover, or Gmail. It can be credentials theft to tweet from company account that the new token is $SCAM and whoever buys first will get airdrop.&lt;/p&gt;

&lt;p&gt;Speaking of phishing - make sure that every team member has &lt;code&gt;MFA&lt;/code&gt; configured. Ideally application-based to avoid &lt;code&gt;SIM swap&lt;/code&gt;s. However, you should be aware that if an active application session is stolen from user browser (via cookie theft) it may completely bypass &lt;code&gt;MFA&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation:&lt;/strong&gt; For max security, use &lt;code&gt;KMS&lt;/code&gt; or hardware-based key management. &lt;code&gt;.env&lt;/code&gt; is not truly secure, but in most cases it is an acceptable tradeoff - just never on a developer's personal laptop with production keys. Dedicated signing machine for key operations only - no browsing, no dev work. Compartmentalize key access by role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; "If your lead dev has the production key on his laptop because he was 'just testing deployment' - that's your biggest vulnerability right now, and no audit will catch it."&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Dependency confusion and Supply Chain
&lt;/h2&gt;

&lt;p&gt;This is trickier, because it often happens through a third-party compromise rather than a direct attack on your infrastructure. Moreover often during audits, it is said: 3rd party integrations are not in scope. And that's fine - but the question is did the team really make a list of external integrations and even threat model it? Are you at least aware what's the worst that can happen if external integrations misbehave?&lt;/p&gt;

&lt;p&gt;On the other hand, some of supply chain attacks may happen immediately. If you are unlucky, the dependency you are just pulling may have been hijacked 4 seconds ago. Attackers know teams auto-update. At current state, the information about compromise travels fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation:&lt;/strong&gt; Pin your dependency versions. Use lockfiles. Review diffs on dependency updates before merging. Maintain a list of your third-party integrations and what access each one has - if one gets compromised, you should know the blast radius without having to figure it out under pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; "The 'we'll just use latest' policy is a bet that every maintainer of every package in your dependency tree will never get compromised. That's a lot of trust in strangers."&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Governance/multisig or malicious employee attack
&lt;/h2&gt;

&lt;p&gt;That's something that's not rarely seen. Especially in remote, distributed teams, you don't always know your peers good enough, and even if you do, there is probably little you can do if they decide to misbehave. Or a colleague may have been compromised - how do you notice, if your colleague is compromised, when he writes to you? Have you ever thought about that?&lt;/p&gt;

&lt;p&gt;For instance, do your team review what is being signed with a multisig? Or does everyone just sign because "something is pushed"? Do you have a process for this?&lt;/p&gt;

&lt;p&gt;Also another thing that is often overlooked - the offboarding. It is super common that former employees remain in shared chat, have shared access etc. And yes, 99% of people are just honest, hardworking humans who simply ignore it, or also don't remember. But there can be 1%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation:&lt;/strong&gt; Map critical procedures and assets. Multisig operations, adding new users, contracts upgrades, funds transfer. The process should assume someone might be dishonest. Yes, this is inconvenient - it is up to you to decide, whether you want to have less flexible process to protect against 1% chance hack though.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Employees and colleagues may be compromised. Enforce MFA (or at least 2FA) on their all accounts. Maintain a list of all accesses granted on onboarding, and revoke all on offboarding.&lt;/p&gt;




&lt;h2&gt;
  
  
  The practical thing: If I were to be a small company CISO, where would I invest?
&lt;/h2&gt;

&lt;p&gt;The company needs to grow. Like a human, who has to care about basic everyday security, he cannot live in a bunker - because despite being safe, he would lose any potential upside life could bring. Hence - how to navigate in this wild space? For a small company, the budget for security is limited and the attack surfaces seem to be infinite. But some of the ideas may be helpful for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow the money - start with the smart contracts / web3 code
&lt;/h3&gt;

&lt;p&gt;Simply, if your main TVL is in your smart contracts, do audit them. However how you do it should be well thought. The more low-hanging fruits are found before the expensive audit, the more time the auditors will spend on actual edge cases, instead of documenting 4 missing &lt;code&gt;onlyOwner&lt;/code&gt; modifiers on admin functions.&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%2F21fnbxsd4xquc1dqhpf2.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%2F21fnbxsd4xquc1dqhpf2.png" alt="5-Stage Audit Approach" width="552" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Ideally the last audit should have very little to no findings. That is the sign that the layered approach worked - all the earlier rounds caught the easy issues and the expensive auditors focused on what matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asset inventory and principle of least privilege
&lt;/h3&gt;

&lt;p&gt;For any other infrastructure you have, especially web servers, websites, check the following things:&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%2Fslwaetuxe8j905ktm6o3.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%2Fslwaetuxe8j905ktm6o3.png" alt="Asset Inventory and Least Privilege" width="696" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Security culture
&lt;/h3&gt;

&lt;p&gt;The company does not need to run monthly phishing simulations or, even worse, run corporate security awareness trainings. However it is simply good to play a long term awareness game and talk security. From time to time, analyze root causes. Find one person that can bring that topic up periodically, e.g. monthly, weekly and simply discuss one recent incident and brainstorm - what the company could do? Would we be protected or would we be hacked?&lt;/p&gt;

&lt;p&gt;In the end also, being secure means being more time consuming to hack than the next similar company in the same risk/reward ratio for the attacker, and not falling for a bait (e.g. dev is compromised on interview). If those things are met, the risk of a real targeted attack, where someone chose your small company, is relatively low.&lt;/p&gt;

&lt;h3&gt;
  
  
  Threat modeling
&lt;/h3&gt;

&lt;p&gt;While professional security services should be performed by entity/company that does this for living (which should guarantee extensive experience), actually a threat model can be done by the team itself, and even shared with the security provider (to better understand what are the main concerns). While there are official methodologies, like &lt;a href="https://owasp.org/www-community/Threat_Modeling_Process" rel="noopener noreferrer"&gt;STRIDE described by OWASP&lt;/a&gt;, even a small exercise enables thinking in terms of adversary mindset. Ask yourself: what if this function is abused. If an attacker wants to steal our money, what would he do? Asking those questions leads to security oriented thinking, which leads to more secure design.&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%2Fdzyeoyiss9slrtdadho6.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%2Fdzyeoyiss9slrtdadho6.png" alt="STRIDE Threat Model" width="696" height="398"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;As a summary, I am going to leave you with a not-very-optimistic conclusion that no matter what you do, the security is NEVER 100% in the same way as humans may &lt;a href="https://en.wikipedia.org/wiki/Death_by_coconut" rel="noopener noreferrer"&gt;die to a coconut&lt;/a&gt;. On the other hand, assuming normal circumstances and no unusual, terrible misfortune or a black swan event, implementing security oriented culture and using security services consciously as described here, increases your chance of incident-free business.&lt;/p&gt;

&lt;p&gt;But if there's one thing to take from this - "no audit covers your ops. No tool covers your people. The weakest link in every incident post-mortem isn't the code - it's a process someone skipped because it was inconvenient that day."&lt;/p&gt;

</description>
      <category>security</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Lagrange interpolation: turning points into a polynomial</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Sun, 12 Apr 2026 13:16:45 +0000</pubDate>
      <link>https://dev.to/0xluk3/lagrange-interpolation-turning-points-into-a-polynomial-l5l</link>
      <guid>https://dev.to/0xluk3/lagrange-interpolation-turning-points-into-a-polynomial-l5l</guid>
      <description>&lt;h2&gt;
  
  
  1. What is a polynomial?
&lt;/h2&gt;

&lt;p&gt;A polynomial is an expression built from a variable x, using only addition, multiplication, and non-negative integer exponents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(x) = 3x² + 2x + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;degree&lt;/strong&gt; is the highest power of x. The example above is degree 2 (quadratic) because x² is the highest term. Some more examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;5x³ − x&lt;/code&gt; - degree 3 (cubic)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;7x + 4&lt;/code&gt; - degree 1 (linear)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;42&lt;/code&gt; - degree 0 (constant)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: &lt;code&gt;7x + 4&lt;/code&gt; has no x on the last term because 4 is really &lt;code&gt;4 · x⁰&lt;/code&gt;, and &lt;code&gt;x⁰ = 1&lt;/code&gt; for any value of x. Every polynomial has an implicit &lt;code&gt;x⁰&lt;/code&gt; on its constant term - we just don't write it.&lt;/p&gt;

&lt;p&gt;The general form of a degree-n polynomial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(x) = aₙxⁿ + aₙ₋₁xⁿ⁻¹ + ⋯ + a₁x + a₀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;a₀, a₁, …, aₙ&lt;/code&gt; are constants called &lt;strong&gt;coefficients&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The setup: points on a graph
&lt;/h2&gt;

&lt;p&gt;You have three data points - expressed as inputs and outputs of some unknown function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(1) = 2,  P(2) = 5,  P(3) = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How many polynomials pass through these points? Through a single point, infinitely many curves fit - constants, lines, parabolas, cubics. Add a second point and you rule out some of them, but many still work. A result in algebra called the &lt;strong&gt;unisolvence theorem&lt;/strong&gt; tells us that &lt;strong&gt;n points pin down exactly one polynomial of degree at most n − 1&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 point - exactly one degree-0 polynomial (a constant)&lt;/li&gt;
&lt;li&gt;2 points - exactly one degree-1 polynomial (a line)&lt;/li&gt;
&lt;li&gt;3 points - exactly one degree-2 polynomial (a parabola)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive visualization:&lt;/strong&gt; The &lt;a href="https://luk3.tech/blog/lagrange-interpolation/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; has a step-through widget here that lets you watch how adding points progressively constrains the polynomial.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For our three points, that unique parabola is &lt;code&gt;P(x) = x² + 1&lt;/code&gt;. No lower-degree polynomial (a line, a constant) can pass through all three - this is the lowest-degree polynomial that fits.&lt;/p&gt;

&lt;p&gt;Finding that polynomial is called &lt;strong&gt;interpolation&lt;/strong&gt;. One way to do it: set up a system of equations. Plug each point into &lt;code&gt;P(x) = ax² + bx + c&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(1):  a + b + c = 2
P(2):  4a + 2b + c = 5
P(3):  9a + 3b + c = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subtract the first equation from the second: &lt;code&gt;3a + b = 3&lt;/code&gt;. Subtract the second from the third: &lt;code&gt;5a + b = 5&lt;/code&gt;. Subtract those two results: &lt;code&gt;2a = 2&lt;/code&gt;, so &lt;code&gt;a = 1&lt;/code&gt;. Back-substitute: &lt;code&gt;b = 0&lt;/code&gt;, &lt;code&gt;c = 1&lt;/code&gt;. The answer is &lt;code&gt;P(x) = x² + 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It works, but it's slow and repetitive - three equations for three points, and it only gets worse as you add more. Joseph-Louis Lagrange found a much more elegant way.&lt;/p&gt;

&lt;p&gt;This matters because ZK proof systems encode computation as polynomials - a prover's data becomes evaluations of a polynomial, and Lagrange interpolation is how that happens. But for now, let's focus purely on the math.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Lagrange's approach: basis polynomials
&lt;/h2&gt;

&lt;p&gt;We still have the same three points: &lt;code&gt;P(1) = 2&lt;/code&gt;, &lt;code&gt;P(2) = 5&lt;/code&gt;, &lt;code&gt;P(3) = 10&lt;/code&gt;. In section 2 we found the polynomial by setting up equations and solving for coefficients. Lagrange's approach reaches the same answer with a completely different method.&lt;/p&gt;

&lt;h3&gt;
  
  
  The goal: three basis polynomials
&lt;/h3&gt;

&lt;p&gt;Our three x-values are 1, 2, 3. Lagrange's idea: build three &lt;strong&gt;basis polynomials&lt;/strong&gt;, one for each x-value. Each one equals 1 at its own x-value and 0 at the other two. We'll call them &lt;code&gt;L₁&lt;/code&gt;, &lt;code&gt;L₂&lt;/code&gt;, &lt;code&gt;L₃&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L₁(x)&lt;/strong&gt; - should equal 1 at x = 1, and zero at x = 2 and x = 3.&lt;/p&gt;

&lt;p&gt;How do we build it? The expression &lt;code&gt;(x − 2)&lt;/code&gt; equals zero when x = 2. The expression &lt;code&gt;(x − 3)&lt;/code&gt; equals zero when x = 3. Multiply them together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(x − 2)(x − 3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything times zero is zero, so this product is zero at both x = 2 and x = 3 - exactly where we need zeros. But what does it give at x = 1?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(1 − 2)(1 − 3) = (−1)(−2) = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We got 2, but we need 1. So we divide the whole thing by 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L₁(x) = (x − 2)(x − 3) / 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify: &lt;code&gt;L₁(1) = (−1)(−2) / 2 = 1&lt;/code&gt;, &lt;code&gt;L₁(2) = (0)(−1) / 2 = 0&lt;/code&gt;, &lt;code&gt;L₁(3) = (1)(0) / 2 = 0&lt;/code&gt;. Exactly what we need.&lt;/p&gt;

&lt;p&gt;If you expand &lt;code&gt;(x − 2)(x − 3) / 2&lt;/code&gt;, you get &lt;code&gt;½x² − ⁵⁄₂x + 3&lt;/code&gt; - a polynomial just like in section 1. But the factored form is the point: it makes the zeros and the normalization obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L₂(x)&lt;/strong&gt; - should equal 1 at x = 2, and zero at x = 1 and x = 3.&lt;/p&gt;

&lt;p&gt;Same recipe. We need zeros at x = 1 and x = 3, so use factors &lt;code&gt;(x − 1)(x − 3)&lt;/code&gt;. Evaluate at x = 2: &lt;code&gt;(2 − 1)(2 − 3) = −1&lt;/code&gt;. Divide by −1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L₂(x) = (x − 1)(x − 3) / (−1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;L₂(1) = 0&lt;/code&gt;, &lt;code&gt;L₂(2) = 1&lt;/code&gt;, &lt;code&gt;L₂(3) = 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L₃(x)&lt;/strong&gt; - should equal 1 at x = 3, and zero at x = 1 and x = 2.&lt;/p&gt;

&lt;p&gt;Factors &lt;code&gt;(x − 1)(x − 2)&lt;/code&gt;. Evaluate at x = 3: &lt;code&gt;(3 − 1)(3 − 2) = 2&lt;/code&gt;. Divide by 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L₃(x) = (x − 1)(x − 2) / 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;L₃(1) = 0&lt;/code&gt;, &lt;code&gt;L₃(2) = 0&lt;/code&gt;, &lt;code&gt;L₃(3) = 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each polynomial equals 1 at one x-value and 0 at the others.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive visualization:&lt;/strong&gt; The &lt;a href="https://luk3.tech/blog/lagrange-interpolation/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; has a step-through widget here that graphs each basis polynomial, showing how it equals 1 at its own point and 0 at the others.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why 1 matters
&lt;/h3&gt;

&lt;p&gt;The number 1 is a neutral multiplier: &lt;code&gt;1 × anything = anything&lt;/code&gt;. Since &lt;code&gt;L₁(1) = 1&lt;/code&gt;, we can multiply by any y-value and deliver it exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 · L₁(1) = 2 · 1 = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And since &lt;code&gt;L₁&lt;/code&gt; is 0 at the other x-values, those stay zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 · L₁(2) = 2 · 0 = 0    2 · L₁(3) = 2 · 0 = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;2 · L₁(x)&lt;/code&gt; gives us exactly 2 at x = 1 and contributes nothing at the other points. Where does the 2 come from? It's the &lt;strong&gt;y-value&lt;/strong&gt; of our first data point: &lt;code&gt;(1, 2)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Combining them
&lt;/h3&gt;

&lt;p&gt;Do the same for each data point. Multiply each &lt;code&gt;Lᵢ&lt;/code&gt; by the y-value of that point and add them up. The y-values come from our data points: &lt;code&gt;(1, 2)&lt;/code&gt;, &lt;code&gt;(2, 5)&lt;/code&gt;, &lt;code&gt;(3, 10)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(x) = 2 · L₁(x) + 5 · L₂(x) + 10 · L₃(x)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check at each point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(1) = 2 · 1 + 5 · 0 + 10 · 0 = 2
P(2) = 2 · 0 + 5 · 1 + 10 · 0 = 5
P(3) = 2 · 0 + 5 · 0 + 10 · 1 = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three data points are hit. Each &lt;code&gt;Lᵢ&lt;/code&gt; delivers its y-value to the right place and contributes nothing elsewhere.&lt;/p&gt;

&lt;p&gt;In general, for n points the i-th basis polynomial is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lᵢ(x) = ∏(j=1 to n, j≠i) [(x − xⱼ) / (xᵢ − xⱼ)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;∏&lt;/code&gt; symbol means "multiply all these together" - like a &lt;code&gt;for&lt;/code&gt; loop that multiplies instead of adding. In pseudocode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;basis&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;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;points&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="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;to&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;if&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;*&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;points&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="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&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;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;points&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="n"&gt;x&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop skips &lt;code&gt;j = i&lt;/code&gt; and multiplies one fraction per remaining point. The numerator &lt;code&gt;(x − xⱼ)&lt;/code&gt; creates a zero at &lt;code&gt;xⱼ&lt;/code&gt;; the denominator &lt;code&gt;(xᵢ − xⱼ)&lt;/code&gt; normalizes to 1 at &lt;code&gt;xᵢ&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The interpolating polynomial
&lt;/h2&gt;

&lt;p&gt;Section 3 built the result for three specific points. The general formula for any set of n points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(x) = ∑(i=1 to n) [yᵢ · Lᵢ(x)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;∑&lt;/code&gt; symbol means "add all these together" - like a &lt;code&gt;for&lt;/code&gt; loop that adds. In pseudocode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;interpolate&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;points&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="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&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;1&lt;/span&gt; &lt;span class="n"&gt;to&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;result&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;points&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;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;basis&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;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;points&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop goes through each data point, computes its basis polynomial at x, multiplies by the y-value, and adds it to the total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is Lagrange interpolation.&lt;/strong&gt; Given any set of points, this formula produces the unique polynomial that passes through all of them. No guessing, no solving equations. You just plug in your points and get the polynomial directly. Every point contributes exactly its y-value at the right place and nothing elsewhere.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive visualization:&lt;/strong&gt; The &lt;a href="https://luk3.tech/blog/lagrange-interpolation/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; has a step-through widget here that shows the weighted basis polynomials combining into the final interpolating polynomial.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; From three basis polynomials &lt;code&gt;L₁&lt;/code&gt;, &lt;code&gt;L₂&lt;/code&gt;, &lt;code&gt;L₃&lt;/code&gt; and three data points &lt;code&gt;(1, 2)&lt;/code&gt;, &lt;code&gt;(2, 5)&lt;/code&gt;, &lt;code&gt;(3, 10)&lt;/code&gt;, we combined them into a weighted sum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(x) = 2 · L₁(x) + 5 · L₂(x) + 10 · L₃(x) = x² + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the unique polynomial that passes through all three points. Each basis polynomial carried one y-value to the right place. The weighted sum produced one new polynomial out of three building blocks.&lt;/p&gt;

&lt;p&gt;One property to keep in mind: change any single data point and the entire polynomial changes. Every point influences every part of the result. This sensitivity is what makes polynomials useful for encoding computation - any error in the input propagates through the entire polynomial.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Why ZK cares
&lt;/h2&gt;

&lt;p&gt;In a ZK proof, the prover encodes their computation as evaluations of a polynomial. The computation produces some set of values at specific points - Lagrange interpolation is how those values become a polynomial.&lt;/p&gt;

&lt;p&gt;The verifier never sees the full polynomial. They see a commitment (a locked-down version of it) and check a few evaluations. The fact that there's exactly one polynomial through any set of points is what makes this work - if the prover's polynomial passes through the right values, it must be the right polynomial.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://interpolationcalculator.com/lagrange-interpolation/" rel="noopener noreferrer"&gt;Lagrange Interpolation Calculator&lt;/a&gt; - plug in your own points and see the polynomial step by step&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Lagrange_polynomial" rel="noopener noreferrer"&gt;Polynomial interpolation - Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>math</category>
      <category>zk</category>
      <category>cryptography</category>
    </item>
    <item>
      <title>The Fiat-Shamir transform: how a hash function replaces a conversation</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Sat, 11 Apr 2026 09:37:28 +0000</pubDate>
      <link>https://dev.to/0xluk3/the-fiat-shamir-transform-how-a-hash-function-replaces-a-conversation-242d</link>
      <guid>https://dev.to/0xluk3/the-fiat-shamir-transform-how-a-hash-function-replaces-a-conversation-242d</guid>
      <description>&lt;p&gt;&lt;em&gt;This post assumes you've read the &lt;a href="https://dev.to/0xluk3/digital-signatures-schnorr-ecdsa-and-how-ps3-was-hacked-4kdk"&gt;previous post on digital signatures&lt;/a&gt;. You should be comfortable with &lt;code&gt;s = k + ex&lt;/code&gt;, the verification equation &lt;code&gt;sG = R + eP&lt;/code&gt;, and why nonce reuse is catastrophic. We're picking up exactly where that left off.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Schnorr as a conversation
&lt;/h2&gt;

&lt;p&gt;Here's something I glossed over in the previous post. The Schnorr scheme we walked through - pick &lt;code&gt;k&lt;/code&gt;, compute &lt;code&gt;R = kG&lt;/code&gt;, hash everything to get &lt;code&gt;e&lt;/code&gt;, compute &lt;code&gt;s = k + ex&lt;/code&gt;, publish &lt;code&gt;(R, s)&lt;/code&gt; - that's the non-interactive version. But Schnorr didn't start there.&lt;/p&gt;

&lt;p&gt;Schnorr originally designed his scheme as an &lt;strong&gt;identification protocol&lt;/strong&gt; - a way for Alice to prove to a server that she's the real account holder. Think of it like logging in, except instead of sending a password, Alice proves she knows the private key &lt;code&gt;x&lt;/code&gt; without ever transmitting it. The protocol was a live conversation between two parties: a prover (Alice) and a verifier (Bob). Three messages, back and forth:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alice → Bob:&lt;/strong&gt; Alice picks a random nonce &lt;code&gt;k&lt;/code&gt;, computes &lt;code&gt;R = kG&lt;/code&gt;, and sends &lt;code&gt;R&lt;/code&gt; to Bob. This is her &lt;strong&gt;commitment&lt;/strong&gt; - she's locked into a particular &lt;code&gt;k&lt;/code&gt; without revealing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bob → Alice:&lt;/strong&gt; Bob picks a random challenge &lt;code&gt;e&lt;/code&gt; and sends it back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alice → Bob:&lt;/strong&gt; Alice computes &lt;code&gt;s = k + ex&lt;/code&gt; and sends &lt;code&gt;s&lt;/code&gt; to Bob. Bob verifies that &lt;code&gt;sG = R + eP&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why does this prove anything? The key is the ordering. Alice commits to &lt;code&gt;R&lt;/code&gt; in step 1 before she knows what &lt;code&gt;e&lt;/code&gt; will be. Bob picks &lt;code&gt;e&lt;/code&gt; randomly in step 2, after he's already received &lt;code&gt;R&lt;/code&gt;. So Alice can't engineer the math - the only way she can produce an &lt;code&gt;s&lt;/code&gt; that satisfies &lt;code&gt;sG = R + eP&lt;/code&gt; for a random &lt;code&gt;e&lt;/code&gt; she didn't choose is if she actually knows &lt;code&gt;x&lt;/code&gt;. Bob's unpredictable challenge is what keeps her honest.&lt;/p&gt;

&lt;p&gt;This protocol works. The question is: &lt;strong&gt;can we take this conversation offline?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On a blockchain, there is no Bob. There's no one to send a live challenge between steps 1 and 3. "Bob" is thousands of nodes verifying a transaction hours or days after it was created. There's no conversation, no real-time back-and-forth - just a signature sitting in a block, waiting to be checked by anyone, at any time.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Fiat-Shamir heuristic: replace Bob with a hash
&lt;/h2&gt;

&lt;p&gt;In 1986, Amos Fiat and Adi Shamir had a deceptively simple idea: &lt;strong&gt;what if the prover generates the challenge herself, by hashing the data she's already committed to?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of waiting for Bob to send a random &lt;code&gt;e&lt;/code&gt;, Alice computes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;e = H(R || P || m)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alice takes her commitment &lt;code&gt;R&lt;/code&gt;, her public key &lt;code&gt;P&lt;/code&gt;, and the message &lt;code&gt;m&lt;/code&gt;, and hashes them together. The output is the challenge &lt;code&gt;e&lt;/code&gt;. No Bob needed.&lt;/p&gt;

&lt;p&gt;Why is this secure? Because Alice commits to &lt;code&gt;R&lt;/code&gt; before she can compute &lt;code&gt;e&lt;/code&gt;. She can't predict what &lt;code&gt;H(R || P || m)&lt;/code&gt; will produce before choosing &lt;code&gt;R&lt;/code&gt;, and she can't reverse-engineer an &lt;code&gt;R&lt;/code&gt; that gives her a convenient &lt;code&gt;e&lt;/code&gt;. The hash binds the challenge to the commitment - exactly the guarantee that Bob's randomness provided in the interactive version.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href="https://www.luk3.tech/blog/fiat-shamir-transform" rel="noopener noreferrer"&gt;original article&lt;/a&gt; includes an interactive demo for this section.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What does verification actually prove?
&lt;/h3&gt;

&lt;p&gt;Say you receive a signature &lt;code&gt;(R, s)&lt;/code&gt; for a message &lt;code&gt;m&lt;/code&gt;, and you know Alice's public key &lt;code&gt;P&lt;/code&gt;. You compute &lt;code&gt;e = H(R || P || m)&lt;/code&gt; and check whether &lt;code&gt;sG = R + eP&lt;/code&gt;. If both sides are equal - the signature is valid. But what does that actually mean?&lt;/p&gt;

&lt;p&gt;The equation &lt;code&gt;sG = R + eP&lt;/code&gt; is really &lt;code&gt;sG = R + e(xG)&lt;/code&gt;, since &lt;code&gt;P = xG&lt;/code&gt;. The only way to produce an &lt;code&gt;s&lt;/code&gt; that makes both sides match is to compute &lt;code&gt;s = k + ex&lt;/code&gt; - which requires knowing the private key &lt;code&gt;x&lt;/code&gt;. You can't work backwards from &lt;code&gt;P&lt;/code&gt; to extract &lt;code&gt;x&lt;/code&gt; (that's the discrete logarithm problem), and you can't guess an &lt;code&gt;s&lt;/code&gt; that works for a random &lt;code&gt;e&lt;/code&gt; you didn't choose. So if the equation holds, whoever produced this signature &lt;strong&gt;must have known &lt;code&gt;x&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The message is protected too. If someone tampers with &lt;code&gt;m&lt;/code&gt;, the recomputed &lt;code&gt;e = H(R || P || m)&lt;/code&gt; changes, the right side of the equation shifts, and verification fails. Same thing if &lt;code&gt;R&lt;/code&gt; is modified, or if the signature was made with a different private key. Any mismatch and the equation breaks.&lt;/p&gt;

&lt;p&gt;Alice proved she owns the private key behind &lt;code&gt;P&lt;/code&gt; without revealing it. That's what every digital signature does - proves control of a secret without exposing it. In fact, this is a zero-knowledge proof - the simplest possible one. The statement being proved is just "I know &lt;code&gt;x&lt;/code&gt; such that &lt;code&gt;P = xG&lt;/code&gt;." Remember that when we get to ZK-SNARKs, where the statement is an entire computation.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The signature: three steps collapsed into one
&lt;/h2&gt;

&lt;p&gt;Let's write out the full Fiat-Shamir-ized Schnorr signature:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signing:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a random nonce &lt;code&gt;k&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compute &lt;code&gt;R = kG&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compute &lt;code&gt;e = H(R || P || m)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compute &lt;code&gt;s = k + ex (mod n)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Publish the signature &lt;code&gt;(R, s)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recompute &lt;code&gt;e = H(R || P || m)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check that &lt;code&gt;sG = R + eP&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Look familiar? This is exactly the Schnorr signing and verification we covered in the previous post. The whole time, we were already using Fiat-Shamir - I just didn't name it. The "challenge" &lt;code&gt;e&lt;/code&gt; that seemed like a straightforward hash was actually doing something profound: replacing an interactive protocol with a non-interactive one.&lt;/p&gt;

&lt;p&gt;The three-message conversation (send &lt;code&gt;R&lt;/code&gt;, receive &lt;code&gt;e&lt;/code&gt;, send &lt;code&gt;s&lt;/code&gt;) collapses into a single step: compute everything locally and publish the result. Anyone can verify later, offline, without participating in a conversation. This is what makes digital signatures possible in decentralized systems - there's no trusted verifier, no live interaction, just math and a hash function.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Random Oracle: why this is secure (with a caveat)
&lt;/h2&gt;

&lt;p&gt;The security proof for Fiat-Shamir relies on a model called the &lt;strong&gt;Random Oracle Model&lt;/strong&gt; (ROM). The idea is simple: pretend the hash function is a perfect black box that outputs truly random values for each unique input. Under this assumption, the hash-generated challenge is indistinguishable from Bob's random challenge, so the non-interactive scheme inherits the security of the interactive one.&lt;/p&gt;

&lt;p&gt;SHA-256 isn't actually a Random Oracle. It's a deterministic algorithm with a fixed structure. But it behaves enough like one that no practical attack has ever exploited the difference. In cryptography, "good enough" is a real thing - if the best known attack requires more computation than there are atoms in the universe, you're fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  ROM vs. Standard Model
&lt;/h3&gt;

&lt;p&gt;Cryptographers distinguish between proofs in the &lt;strong&gt;Random Oracle Model&lt;/strong&gt; and proofs in the &lt;strong&gt;Standard Model&lt;/strong&gt; (which makes no idealized assumptions about hash functions).&lt;/p&gt;

&lt;p&gt;ROM proofs are sometimes criticized because there exist contrived, artificial schemes that are provably secure in the ROM but broken when any concrete hash function is substituted. These counterexamples are deliberately constructed to make the point - they don't correspond to real-world schemes.&lt;/p&gt;

&lt;p&gt;In practice, every major scheme using Fiat-Shamir (Schnorr, Ed25519, BIP340) has decades of cryptanalysis behind it and no real-world attacks exploiting the ROM gap. The theoretical concern is real but the practical impact has been zero.&lt;/p&gt;

&lt;p&gt;Some schemes (like certain lattice-based signatures) do achieve security in the Standard Model, but they typically pay a cost in signature size or computation. For elliptic curve cryptography, ROM-based proofs remain the norm.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Why this matters: from conversations to blockchains
&lt;/h2&gt;

&lt;p&gt;Fiat-Shamir gives us three things that make modern cryptographic systems work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-interactivity.&lt;/strong&gt; A signature is created once and verified by anyone, anytime. No live connection, no trusted verifier, no conversation. This is essential for blockchains, where a transaction might be verified by thousands of nodes months after it was signed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Determinism.&lt;/strong&gt; Given the same inputs (&lt;code&gt;k&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt;), the signature is always the same. Combined with deterministic nonce generation (RFC 6979), the entire signing process is reproducible - no randomness needed at verification time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security.&lt;/strong&gt; The hash function binds the challenge to the commitment. Changing any input - the nonce, the public key, the message - produces a completely different challenge. There's no wiggle room.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href="https://www.luk3.tech/blog/fiat-shamir-transform" rel="noopener noreferrer"&gt;original article&lt;/a&gt; includes an interactive demo for this section.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And here's the thing that makes Fiat-Shamir more than a historical footnote: &lt;strong&gt;it's everywhere&lt;/strong&gt;. Every time a cryptographic protocol needs to turn an interactive proof into a non-interactive one, Fiat-Shamir is the tool. Schnorr signatures use it. Ed25519 uses it. But it goes far beyond signatures.&lt;/p&gt;

&lt;p&gt;ZK-SNARKs use Fiat-Shamir. STARKs use Fiat-Shamir. Bulletproofs use Fiat-Shamir. Every modern zero-knowledge proof system that produces a non-interactive proof is applying the same trick: replace the verifier's random challenge with a hash of the prover's commitments.&lt;/p&gt;

&lt;p&gt;In the Schnorr case, the statement being proved is "I know the private key &lt;code&gt;x&lt;/code&gt;." But what if the statement were something more complex - like "I know an input that makes this entire computation produce this output"? That's where zero-knowledge proofs go next, and Fiat-Shamir is the bridge that makes them practical.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>math</category>
      <category>blockchain</category>
      <category>security</category>
    </item>
    <item>
      <title>Digital signatures: Schnorr, ECDSA and how PS3 was hacked</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Fri, 10 Apr 2026 07:54:45 +0000</pubDate>
      <link>https://dev.to/0xluk3/digital-signatures-schnorr-ecdsa-and-how-ps3-was-hacked-4kdk</link>
      <guid>https://dev.to/0xluk3/digital-signatures-schnorr-ecdsa-and-how-ps3-was-hacked-4kdk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part two. If you haven't read it yet, start with &lt;a href="https://dev.to/0xluk3/private-keys-and-elliptic-curves-a-deep-dive-for-people-who-dont-like-math-jad"&gt;Private keys and elliptic curves: a deep-dive for people who don't like math&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a digital signature?
&lt;/h2&gt;

&lt;p&gt;You know how a handwritten signature works. You scribble something on a document and everyone agrees it proves you authorized it. Digital signatures do the same thing, except they're actually secure.&lt;/p&gt;

&lt;p&gt;A digital signature has three properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; - it proves the signer has the private key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity&lt;/strong&gt; - it proves the message hasn't been tampered with&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-repudiation&lt;/strong&gt; - the signer can't later deny signing it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In blockchain, every transaction you send is signed with your private key. The network verifies that signature using your public key - without ever learning the key itself. No trust needed. Just math.&lt;/p&gt;

&lt;p&gt;There are two signature schemes that matter here: &lt;strong&gt;Schnorr&lt;/strong&gt; and &lt;strong&gt;ECDSA&lt;/strong&gt;. Schnorr is more "classic" math and easier to understand, so we'll start with it. ECDSA is the one everyone actually uses - for historical reasons we'll get into.&lt;/p&gt;

&lt;p&gt;Bear with me.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Schnorr signatures: the ingredients
&lt;/h2&gt;

&lt;p&gt;Quick refresher from the previous post. We have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;G&lt;/strong&gt; - the generator point (public, same for everyone)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;x&lt;/strong&gt; - the private key (secret, a scalar)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P = xG&lt;/strong&gt; - the public key (public, a point on the curve)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Schnorr signature introduces one new ingredient: a &lt;strong&gt;nonce&lt;/strong&gt;. This is a random number &lt;code&gt;k&lt;/code&gt;, freshly generated for every single signature. Think of it as a one-time secret that makes each signature unique. Only the signer knows &lt;code&gt;k&lt;/code&gt; — it is never revealed to the verifier, because anyone who learns &lt;code&gt;k&lt;/code&gt; can trivially extract the private key from &lt;code&gt;s = k + ex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;k&lt;/code&gt;, we compute the &lt;strong&gt;nonce commitment&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Just like the public key is &lt;code&gt;k&lt;/code&gt; times &lt;code&gt;G&lt;/code&gt;, the nonce commitment is &lt;code&gt;k&lt;/code&gt; times &lt;code&gt;G&lt;/code&gt;. Same operation, different secret. &lt;code&gt;R&lt;/code&gt; is a point on the curve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signing
&lt;/h3&gt;

&lt;p&gt;To sign a message &lt;code&gt;m&lt;/code&gt; (in practice, &lt;code&gt;m&lt;/code&gt; is the hash of the original message - a function like SHA-256 turns "hello world" into a fixed-size 256-bit number, so it can be used in math):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1.&lt;/strong&gt; Pick a random nonce &lt;code&gt;k&lt;/code&gt;, compute &lt;code&gt;R = kG&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2.&lt;/strong&gt; Compute the challenge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;e = H(R || P || m)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;H&lt;/code&gt; is a hash function (like SHA-256) and &lt;code&gt;||&lt;/code&gt; means concatenation. The challenge &lt;code&gt;e&lt;/code&gt; is a scalar - just a regular number, living in the same modular arithmetic space as the private key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3.&lt;/strong&gt; Compute the signature scalar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s = k + ex  (mod n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;n&lt;/code&gt; is the order of the curve (the number of points in the group generated by &lt;code&gt;G&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The signature is the pair &lt;code&gt;(R, s)&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href="https://www.luk3.tech/blog/digital-signatures-schnorr-ecdsa" rel="noopener noreferrer"&gt;original article&lt;/a&gt; includes an interactive demo for this section.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Why does e include P and R? (the chicken-and-egg thing)
&lt;/h4&gt;

&lt;p&gt;You might wonder: if the challenge &lt;code&gt;e&lt;/code&gt; depends on &lt;code&gt;R&lt;/code&gt; (which the signer computes), can't the signer cheat by picking &lt;code&gt;e&lt;/code&gt; first and then engineering &lt;code&gt;R&lt;/code&gt; to match?&lt;/p&gt;

&lt;p&gt;That's exactly why &lt;code&gt;e&lt;/code&gt; is computed as a hash of &lt;code&gt;R&lt;/code&gt;, &lt;code&gt;P&lt;/code&gt;, and &lt;code&gt;m&lt;/code&gt; together. The hash function makes it computationally impossible to pick &lt;code&gt;R&lt;/code&gt; such that &lt;code&gt;H(R || P || m)&lt;/code&gt; gives you a convenient &lt;code&gt;e&lt;/code&gt;. You'd need to break the hash function to do it.&lt;/p&gt;

&lt;p&gt;This also means there's a chicken-and-egg: you need &lt;code&gt;R&lt;/code&gt; to compute &lt;code&gt;e&lt;/code&gt;, and you need &lt;code&gt;e&lt;/code&gt; to compute &lt;code&gt;s&lt;/code&gt;. But you don't need &lt;code&gt;e&lt;/code&gt; to compute &lt;code&gt;R&lt;/code&gt; - &lt;code&gt;R = kG&lt;/code&gt; is independent. So the order is: pick &lt;code&gt;k&lt;/code&gt;, compute &lt;code&gt;R&lt;/code&gt;, then compute &lt;code&gt;e&lt;/code&gt;, then compute &lt;code&gt;s&lt;/code&gt;. No contradiction.&lt;/p&gt;

&lt;p&gt;One consequence: because &lt;code&gt;e&lt;/code&gt; includes &lt;code&gt;P&lt;/code&gt;, you can't recover the public key from a Schnorr signature alone. You need to already know &lt;code&gt;P&lt;/code&gt; to verify. This is different from ECDSA, where public key recovery is possible - more on that later.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Schnorr verification: why it works
&lt;/h2&gt;

&lt;p&gt;The verifier has: the message &lt;code&gt;m&lt;/code&gt;, the public key &lt;code&gt;P&lt;/code&gt;, and the signature &lt;code&gt;(R, s)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1.&lt;/strong&gt; Recompute the challenge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;e = H(R || P || m)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2.&lt;/strong&gt; Check whether:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sG = R + eP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both sides are equal, the signature is valid.&lt;/p&gt;

&lt;p&gt;That's the entire verification. Compute both sides of the equation, check if they match. Let's walk through why this works.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href="https://www.luk3.tech/blog/digital-signatures-schnorr-ecdsa" rel="noopener noreferrer"&gt;original article&lt;/a&gt; includes an interactive demo for this section.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Algebraic proof
&lt;/h4&gt;

&lt;p&gt;We know that &lt;code&gt;s = k + ex (mod n)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Multiply both sides by &lt;code&gt;G&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sG = (k + ex)G
sG = kG + exG
sG = kG + e(xG)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;kG = R&lt;/code&gt; (nonce commitment) and &lt;code&gt;xG = P&lt;/code&gt; (public key), so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sG = R + eP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The equation checks out because of how &lt;code&gt;s&lt;/code&gt; was constructed. The private key &lt;code&gt;x&lt;/code&gt; is hidden inside &lt;code&gt;s&lt;/code&gt;, combined with the nonce &lt;code&gt;k&lt;/code&gt; in a way that the verifier can confirm without ever extracting either secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why can't you fake it?
&lt;/h3&gt;

&lt;p&gt;Three attack scenarios and why they all fail:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attack 1: Forge without knowing x.&lt;/strong&gt; You'd need to produce &lt;code&gt;s&lt;/code&gt; such that &lt;code&gt;sG = R + eP&lt;/code&gt;. Since &lt;code&gt;e&lt;/code&gt; depends on &lt;code&gt;R&lt;/code&gt; (through the hash), you can't choose &lt;code&gt;R&lt;/code&gt; and &lt;code&gt;e&lt;/code&gt; independently. You'd have to solve the discrete logarithm problem, which is computationally infeasible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attack 2: Reuse someone else's signature.&lt;/strong&gt; The challenge &lt;code&gt;e&lt;/code&gt; includes the message &lt;code&gt;m&lt;/code&gt;. A different message produces a different &lt;code&gt;e&lt;/code&gt;, which requires a different &lt;code&gt;s&lt;/code&gt;. Old signatures don't work on new messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attack 3: Modify the message after signing.&lt;/strong&gt; Changing even one bit of &lt;code&gt;m&lt;/code&gt; changes &lt;code&gt;e&lt;/code&gt; (because hash functions), which breaks the equation &lt;code&gt;sG = R + eP&lt;/code&gt;. Tampering is immediately detectable.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. ECDSA: the one everyone uses
&lt;/h2&gt;

&lt;p&gt;Schnorr signatures were invented in 1989 by Claus-Peter Schnorr. They're clean, efficient, and mathematically elegant. So naturally, the world didn't use them - because Schnorr patented the algorithm.&lt;/p&gt;

&lt;p&gt;The patent expired in 2008, but by then ECDSA (Elliptic Curve Digital Signature Algorithm) was already the standard. ECDSA was designed as a patent-free alternative, and it's what Bitcoin, Ethereum, and most of the blockchain world run on. Bitcoin finally adopted Schnorr in the 2021 Taproot upgrade (BIP340), but ECDSA remains dominant.&lt;/p&gt;

&lt;h3&gt;
  
  
  ECDSA signing
&lt;/h3&gt;

&lt;p&gt;Same ingredients: private key &lt;code&gt;x&lt;/code&gt;, nonce &lt;code&gt;k&lt;/code&gt;, message &lt;code&gt;m&lt;/code&gt;. But the math is different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1.&lt;/strong&gt; Pick random nonce &lt;code&gt;k&lt;/code&gt;, compute &lt;code&gt;R = kG&lt;/code&gt;. Let &lt;code&gt;r&lt;/code&gt; be the x-coordinate of &lt;code&gt;R&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2.&lt;/strong&gt; Compute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s = k⁻¹(m + rx)  (mod n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;k⁻¹&lt;/code&gt; is the inverse of &lt;code&gt;k&lt;/code&gt; (the number that satisfies &lt;code&gt;k * k⁻¹ = 1 mod n&lt;/code&gt; - think of it as division in modular arithmetic).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The signature is &lt;code&gt;(r, s)&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Notice the difference: Schnorr uses &lt;code&gt;s = k + ex&lt;/code&gt;, which is linear. ECDSA uses &lt;code&gt;s = k⁻¹(m + rx)&lt;/code&gt;, which involves a modular inverse. That &lt;code&gt;k⁻¹&lt;/code&gt; makes ECDSA fundamentally non-linear, and that non-linearity has consequences.&lt;/p&gt;

&lt;h3&gt;
  
  
  The r, s, and v of Ethereum
&lt;/h3&gt;

&lt;p&gt;If you've looked at Ethereum transactions, you've seen three values: &lt;code&gt;v&lt;/code&gt;, &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;r&lt;/strong&gt; - the x-coordinate of the nonce point &lt;code&gt;R = kG&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;s&lt;/strong&gt; - the signature scalar from the formula above&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v&lt;/strong&gt; - the recovery ID (27 or 28, or 0/1 in EIP-155)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An ECDSA signature is 65 bytes: 32 bytes for &lt;code&gt;r&lt;/code&gt;, 32 bytes for &lt;code&gt;s&lt;/code&gt;, and 1 byte for &lt;code&gt;v&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ecrecover: public key recovery
&lt;/h3&gt;

&lt;p&gt;Here's something ECDSA can do that Schnorr can't: &lt;strong&gt;recover the public key from the signature alone&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Given &lt;code&gt;(r, s, v)&lt;/code&gt; and the message hash &lt;code&gt;m&lt;/code&gt;, the &lt;code&gt;ecrecover&lt;/code&gt; function reconstructs the signer's public key &lt;code&gt;P&lt;/code&gt;. This is possible because the ECDSA equation links &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt;, and &lt;code&gt;P&lt;/code&gt; in a way that lets you solve for &lt;code&gt;P&lt;/code&gt; if you know the other three. The &lt;code&gt;v&lt;/code&gt; byte disambiguates which of two possible points &lt;code&gt;R&lt;/code&gt; was (since an x-coordinate on the curve maps to two y-values).&lt;/p&gt;

&lt;p&gt;Ethereum uses &lt;code&gt;ecrecover&lt;/code&gt; heavily. Instead of storing the sender's public key in every transaction, the network recovers it from the signature. This saves space and is why Ethereum addresses are derived from public keys rather than included explicitly.&lt;/p&gt;

&lt;p&gt;Schnorr can't do this because the challenge &lt;code&gt;e = H(R || P || m)&lt;/code&gt; includes &lt;code&gt;P&lt;/code&gt;. You'd need to know &lt;code&gt;P&lt;/code&gt; to compute &lt;code&gt;e&lt;/code&gt;, which you need to verify the signature. Chicken and egg - but in Schnorr's case, it's a feature, not a bug.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Schnorr vs ECDSA
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Schnorr&lt;/th&gt;
&lt;th&gt;ECDSA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signature size&lt;/td&gt;
&lt;td&gt;64 bytes (R, s)&lt;/td&gt;
&lt;td&gt;65 bytes (r, s, v)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s = k + ex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s = k⁻¹(m + rx)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch verification&lt;/td&gt;
&lt;td&gt;Native (via linearity)&lt;/td&gt;
&lt;td&gt;Not natively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malleable&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (needs low-s rule)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key recovery&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Yes (ecrecover)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature aggregation&lt;/td&gt;
&lt;td&gt;Yes (MuSig)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why Schnorr's linearity matters
&lt;/h3&gt;

&lt;p&gt;The Schnorr equation &lt;code&gt;s = k + ex&lt;/code&gt; is &lt;strong&gt;linear&lt;/strong&gt; in both &lt;code&gt;k&lt;/code&gt; and &lt;code&gt;x&lt;/code&gt;. This has a powerful consequence: signatures can be &lt;strong&gt;aggregated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Alice signs with &lt;code&gt;s₁ = k₁ + ex₁&lt;/code&gt; and Bob signs with &lt;code&gt;s₂ = k₂ + ex₂&lt;/code&gt;, you can combine them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s = s₁ + s₂ = (k₁ + k₂) + e(x₁ + x₂)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a valid Schnorr signature for the combined public key &lt;code&gt;P₁ + P₂&lt;/code&gt;. One signature, one verification, multiple signers. This is the foundation of multisig schemes like MuSig, and it's why Taproot transactions on Bitcoin are cheaper - an n-of-n multisig looks identical to a single-signer transaction on chain.&lt;/p&gt;

&lt;p&gt;ECDSA can't do this. The &lt;code&gt;k⁻¹&lt;/code&gt; term makes the equation non-linear, so adding two ECDSA signatures together produces garbage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; Naive Schnorr aggregation (just adding keys and nonces) is vulnerable to rogue-key attacks. Production schemes like MuSig and MuSig2 add a commitment round to prevent this. The linearity enables aggregation, but safe aggregation requires a protocol on top.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ECDSA malleability
&lt;/h3&gt;

&lt;p&gt;ECDSA has a quirk: for any valid signature &lt;code&gt;(r, s)&lt;/code&gt;, the pair &lt;code&gt;(r, n - s)&lt;/code&gt; is also a valid signature for the same message. This is called &lt;strong&gt;signature malleability&lt;/strong&gt; - a third party can "flip" your signature without knowing your private key.&lt;/p&gt;

&lt;p&gt;This caused real problems. On early Bitcoin, malleability allowed transaction IDs to be changed after broadcast (since the txid included the signature), which broke systems that tracked transactions by ID.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;the low-s rule&lt;/strong&gt; (BIP 62, and later EIP-2 for Ethereum). Valid signatures must have &lt;code&gt;s&lt;/code&gt; in the lower half of the range (&lt;code&gt;s &amp;lt;= n/2&lt;/code&gt;). If &lt;code&gt;s&lt;/code&gt; is in the upper half, the signer must replace it with &lt;code&gt;n - s&lt;/code&gt;. This makes each signature unique and eliminates malleability.&lt;/p&gt;

&lt;p&gt;Schnorr doesn't have this problem. The signature includes the full point &lt;code&gt;R&lt;/code&gt; (not just an x-coordinate), so there's no ambiguity to exploit.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The nonce catastrophe
&lt;/h2&gt;

&lt;p&gt;Everything above assumes one thing: &lt;strong&gt;the nonce &lt;code&gt;k&lt;/code&gt; is unique and secret for every signature.&lt;/strong&gt; If it's not, you're gonna have a bad time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nonce reuse in Schnorr
&lt;/h3&gt;

&lt;p&gt;If you sign two different messages &lt;code&gt;m₁&lt;/code&gt; and &lt;code&gt;m₂&lt;/code&gt; with the same nonce &lt;code&gt;k&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s₁ = k + e₁x
s₂ = k + e₂x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subtract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s₁ - s₂ = (e₁ - e₂)x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Solve for the private key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x = (s₁ - s₂) / (e₁ - e₂)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two signatures, same nonce, your private key is gone. Simple division.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nonce reuse in ECDSA
&lt;/h3&gt;

&lt;p&gt;Same disaster, slightly different algebra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s₁ = k⁻¹(m₁ + rx)
s₂ = k⁻¹(m₂ + rx)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;k&lt;/code&gt; and &lt;code&gt;r&lt;/code&gt; are the same (same nonce means same &lt;code&gt;R&lt;/code&gt;, same &lt;code&gt;r&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s₁ - s₂ = k⁻¹(m₁ - m₂)
k = (m₁ - m₂) / (s₁ - s₂)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have &lt;code&gt;k&lt;/code&gt;, extract &lt;code&gt;x&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x = (s₁k - m₁) / r
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Game over.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PS3 hack: a real-world catastrophe
&lt;/h3&gt;

&lt;p&gt;This isn't theoretical. In 2010, &lt;strong&gt;fail0verflow&lt;/strong&gt; demonstrated this exact attack against Sony's PlayStation 3.&lt;/p&gt;

&lt;p&gt;Sony used ECDSA to sign all PS3 software updates and games. The private signing key was the ultimate safeguard - without it, nobody could produce valid signed code for the console. The entire security model depended on that key staying secret.&lt;/p&gt;

&lt;p&gt;The problem: Sony's implementation used a &lt;strong&gt;static nonce&lt;/strong&gt;. Not a weak random number generator, not a biased nonce - a constant. Every single signature Sony produced used the same value of &lt;code&gt;k&lt;/code&gt;. That means anyone with two signed firmware updates could apply the algebra above and extract Sony's private signing key. And that's exactly what happened.&lt;/p&gt;

&lt;p&gt;With the key, anyone could sign arbitrary code and run it on any PS3. The console's entire code-signing security model was broken - permanently and irreversibly. Sony couldn't fix it with a software update because the compromised key was the root signing key. Every PS3 ever manufactured trusted that key. The hardware couldn't be patched.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: deterministic nonces (RFC 6979)
&lt;/h3&gt;

&lt;p&gt;The root cause of these catastrophes is relying on a random number generator to produce &lt;code&gt;k&lt;/code&gt;. If the RNG is broken, biased, or reused, the private key leaks.&lt;/p&gt;

&lt;p&gt;RFC 6979 eliminates this by making the nonce &lt;strong&gt;deterministic&lt;/strong&gt;: &lt;code&gt;k = HMAC(x, m)&lt;/code&gt; - derived from the private key and the message using HMAC. The same message and key always produce the same &lt;code&gt;k&lt;/code&gt; (so signing is reproducible), but different messages produce different &lt;code&gt;k&lt;/code&gt; values (so there's no reuse). No randomness needed, no RNG to fail.&lt;/p&gt;

&lt;p&gt;Most modern ECDSA implementations use RFC 6979 or something equivalent. Schnorr (BIP340) similarly recommends deterministic nonce generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So that's digital signatures. A private key, a nonce, a hash, and some elliptic curve arithmetic - and you get a proof that someone authorized something without ever revealing their secret. Schnorr does it with a clean linear equation, ECDSA does it with a modular inverse and a patent-free workaround that became the global standard. Both break catastrophically if you reuse a nonce, and both work beautifully if you don't.&lt;/p&gt;

&lt;p&gt;If you made it through this and the previous post, you now understand the core cryptography underneath every blockchain transaction. Not bad for someone who doesn't like math.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>math</category>
      <category>blockchain</category>
      <category>security</category>
    </item>
    <item>
      <title>Private keys and elliptic curves: a deep-dive for people who don't like math</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Thu, 09 Apr 2026 07:15:19 +0000</pubDate>
      <link>https://dev.to/0xluk3/private-keys-and-elliptic-curves-a-deep-dive-for-people-who-dont-like-math-jad</link>
      <guid>https://dev.to/0xluk3/private-keys-and-elliptic-curves-a-deep-dive-for-people-who-dont-like-math-jad</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;If you ever wondered how elliptic curves and private keys actually work, and you never liked math like me - let's try to get the idea without too much of it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  First: why does any of this exist?
&lt;/h2&gt;

&lt;p&gt;Blockchain needs to solve one problem: &lt;strong&gt;how do you prove you own something without telling anyone your secret?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your private key is the secret. Your public key (and address) is what you share with the world. The cryptography underneath ensures that knowing your public key reveals absolutely nothing about your private key - even though one is mathematically derived from the other.&lt;/p&gt;

&lt;p&gt;The math that makes this possible relies on specific properties of algebraic structures that are easy to compute in one direction but practically impossible to reverse.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. What even is a private key?
&lt;/h2&gt;

&lt;p&gt;A private key is just a number. An absurdly large number - on Bitcoin's curve, it's a random integer between 1 and roughly:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You can think of it as a 256-bit random number. If you somehow guessed everyone's private key by brute force, you'd need to try more combinations than there are atoms in the observable universe. So that's fine. Very secure. Moving on.&lt;/p&gt;

&lt;p&gt;The public key is derived from the private key - we'll come back to exactly how at the end of the article. For now, let's focus on the private key itself and how it's possible that everyone gets a unique one.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Elliptic curves: a special kind of function
&lt;/h2&gt;

&lt;p&gt;Elliptic curves are the foundation of all this. Think of them as a twisted version of the linear function you know from primary school.&lt;/p&gt;

&lt;p&gt;You know linear functions. &lt;code&gt;y = ax + b&lt;/code&gt;. Grows in a straight line. Boring but useful.&lt;/p&gt;

&lt;p&gt;You probably also know quadratics - parabolas and such. Those grow faster, curve around, have some nice properties.&lt;/p&gt;

&lt;p&gt;Elliptic curves are a different beast entirely. The general form used in cryptography is called the &lt;strong&gt;Weierstrass form&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;y² = x³ + ax + b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Bitcoin specifically (the &lt;strong&gt;secp256k1&lt;/strong&gt; curve), &lt;code&gt;a = 0&lt;/code&gt; and &lt;code&gt;b = 7&lt;/code&gt;, giving you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;y² = x³ + 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;secp256k1 is just a specific name for this particular curve with this particular formula. The math behind elliptic curve cryptography was once purely theoretical academic stuff - until someone applied it in web3 to build magic internet money.&lt;/p&gt;

&lt;p&gt;Plot that over real numbers and you get something that looks like a smooth, continuous snake curving through the plane - symmetric about the x-axis, because squaring &lt;code&gt;y&lt;/code&gt; means both &lt;code&gt;+y&lt;/code&gt; and &lt;code&gt;-y&lt;/code&gt; are solutions.&lt;/p&gt;

&lt;p&gt;Elliptic curves are also used in tons of places outside blockchain - TLS, SSH, Signal encryption - but in this post we only care about one thing they can do: &lt;strong&gt;a weird kind of arithmetic that's hard to reverse&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Correctness note:&lt;/strong&gt; The curve equation you often see written as &lt;code&gt;y² = ax³ + 7&lt;/code&gt; is slightly off. The &lt;code&gt;a&lt;/code&gt; coefficient belongs to the &lt;code&gt;x&lt;/code&gt; term, not the &lt;code&gt;x³&lt;/code&gt; term. The actual secp256k1 curve is &lt;code&gt;y² = x³ + 7&lt;/code&gt;. Small difference, matters if you implement it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Point addition: geometry as arithmetic
&lt;/h2&gt;

&lt;p&gt;Here's where elliptic curves get interesting.&lt;/p&gt;

&lt;p&gt;You can "add" two points on an elliptic curve together. This sounds made up, but it has a precise geometric definition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To add point P and point Q:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draw a straight line through P and Q&lt;/li&gt;
&lt;li&gt;That line intersects the curve at exactly one more point&lt;/li&gt;
&lt;li&gt;Reflect that intersection point across the x-axis&lt;/li&gt;
&lt;li&gt;That reflected point is &lt;code&gt;P + Q&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not addition in any normal sense. No numbers are being summed. You're doing geometry, and calling it addition - but it behaves like addition (associative, commutative, has an identity element), so mathematicians are happy to call it that.&lt;/p&gt;

&lt;p&gt;The interactive below shows this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The &lt;a href="https://luk3.tech/blog/private-keys-elliptic-curves/" rel="noopener noreferrer"&gt;original article&lt;/a&gt; includes an interactive demo for this section.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The actual formula (you don't need to learn this)&lt;/p&gt;

&lt;p&gt;Given points P = (x₁, y₁) and Q = (x₂, y₂) on the curve:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  λ = (y₂ - y₁) / (x₂ - x₁)

  x₃ = λ² - x₁ - x₂
  y₃ = λ(x₁ - x₃) - y₁
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The result P + Q = (x₃, y₃).&lt;/p&gt;

&lt;p&gt;There are also edge cases: adding a point to itself (called &lt;strong&gt;point doubling&lt;/strong&gt;) uses a different formula involving the curve's derivative. You won't need to implement this by hand in your life. The point is: &lt;em&gt;there's a formula&lt;/em&gt;, and it's deterministic.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Scalar multiplication: the secret weapon
&lt;/h2&gt;

&lt;p&gt;Now that we can add two points, we can do &lt;strong&gt;scalar multiplication&lt;/strong&gt;: multiplying a point by a number &lt;code&gt;k&lt;/code&gt; means adding it to itself &lt;code&gt;k&lt;/code&gt; times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k × P = P + P + P + ... (k times)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The &lt;a href="https://luk3.tech/blog/private-keys-elliptic-curves/" rel="noopener noreferrer"&gt;original article&lt;/a&gt; includes an interactive demo for this section.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the core operation in elliptic curve cryptography. If &lt;code&gt;G&lt;/code&gt; is a fixed starting point on the curve (called the &lt;strong&gt;generator point&lt;/strong&gt; - the same for everyone using secp256k1), and &lt;code&gt;k&lt;/code&gt; is your private key, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public Key = k × G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your public key is the result of scalar-multiplying the generator point by your private key.&lt;/p&gt;

&lt;p&gt;Now here's the key question your brain is probably asking: &lt;em&gt;"If I know G and I know the public key, can't I just figure out k by reversing the multiplication?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And here's where it gets delicious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In principle, yes.&lt;/strong&gt; If you had infinite time and infinite compute, you could just try every value of k from 1 upward until you found one that produced your public key. This is called the &lt;strong&gt;Discrete Logarithm Problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In practice, no&lt;/strong&gt; - because of finite fields. Which brings us to the twist.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Finite fields: the real story
&lt;/h2&gt;

&lt;p&gt;Now here's the moment of truth. The elliptic curve you've probably seen thousands of times - that smooth snake shape - is not real. There is no real snake-shaped elliptic curve in blockchain. That's why what follows matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the truth:&lt;/strong&gt; The elliptic curve operations in Bitcoin and Ethereum don't happen over real numbers. They happen over a &lt;strong&gt;finite field&lt;/strong&gt; - also called a &lt;strong&gt;Galois field&lt;/strong&gt; or &lt;strong&gt;prime field&lt;/strong&gt;, written as &lt;strong&gt;GF(p)&lt;/strong&gt; or &lt;strong&gt;𝔽ₚ&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's a finite field?
&lt;/h3&gt;

&lt;p&gt;A finite field is a "closed universe" of numbers with specific rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It contains exactly &lt;code&gt;p&lt;/code&gt; elements (where &lt;code&gt;p&lt;/code&gt; is a prime number)&lt;/li&gt;
&lt;li&gt;All arithmetic is done &lt;strong&gt;modulo p&lt;/strong&gt; - meaning results "wrap around" when they exceed p&lt;/li&gt;
&lt;li&gt;Every element has a multiplicative inverse (division always works)&lt;/li&gt;
&lt;li&gt;The set is closed: you can never escape it through arithmetic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of modular arithmetic like a clock - or like an unsigned integer that overflows. If your variable holds values &lt;code&gt;0&lt;/code&gt; through &lt;code&gt;7&lt;/code&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 + 4 = 8 → wraps to 0
4 + 6 = 10 → wraps to 2
7 + 7 = 14 → wraps to 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;arithmetic modulo 8&lt;/strong&gt; - every result is taken &lt;code&gt;mod 8&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters for cryptography
&lt;/h3&gt;

&lt;p&gt;Here's the clever bit. Say someone knows you did some additions modulo 8, and the result is &lt;code&gt;4&lt;/code&gt;. They can't tell whether you computed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 + 3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4 + 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2 + 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4 + 8&lt;/code&gt; (one full wrap)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4 + 16&lt;/code&gt; (two wraps)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4 + 10000000&lt;/code&gt; (a lot of wraps)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You cannot recover the inputs from the output.&lt;/strong&gt; The modular wrapping destroys that information. This is one of the ways to construct trapdoor (one-way) functions in cryptography.&lt;/p&gt;

&lt;p&gt;Bitcoin's prime &lt;code&gt;p&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p = 2²⁵⁶ - 2³² - 977
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not 8 possible values. That's a number with 77 decimal digits. The wrapping is... extensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Elliptic curves over finite fields: the spilled rice
&lt;/h2&gt;

&lt;p&gt;Here's what happens when you take the elliptic curve equation and restrict it to a finite field:&lt;/p&gt;

&lt;p&gt;Instead of computing &lt;code&gt;y² = x³ + 7&lt;/code&gt; over all real numbers (giving you a smooth curve), you compute it over &lt;strong&gt;integers mod p&lt;/strong&gt; - meaning x and y can only be integers from 0 to p-1, and the &lt;code&gt;=&lt;/code&gt; sign means "equals modulo p."&lt;/p&gt;

&lt;p&gt;The result looks nothing like a snake. It looks like rice that someone spilled on a floor - a scatter of disconnected points with no obvious pattern. There's no adjacency, no gradient, no smooth path you could walk along. Each point lands somewhere based on the modular arithmetic, which causes values to wrap around unpredictably.&lt;/p&gt;

&lt;p&gt;This is still technically an "elliptic curve" - the same algebraic structure applies, point addition still works, the formula still holds - but geometrically, it's unrecognizable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The &lt;a href="https://luk3.tech/blog/private-keys-elliptic-curves/" rel="noopener noreferrer"&gt;original article&lt;/a&gt; includes an interactive demo for this section.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why does the spilled rice matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On a smooth curve, you might imagine "walking backwards" from the public key to the private key - following the curve, tracing the path of scalar multiplications. There's a visual intuition for reversal.&lt;/p&gt;

&lt;p&gt;On the spilled rice? There's no path. There's no adjacency. You can't walk anywhere. You can only try individual points at random and check if they match.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;p&lt;/code&gt; having 77 decimal digits, there are roughly &lt;code&gt;p/2&lt;/code&gt; valid curve points. The number of multiplications you'd need to try to brute-force a 256-bit private key is astronomically large. This is the &lt;strong&gt;Elliptic Curve Discrete Logarithm Problem (ECDLP)&lt;/strong&gt;, and no efficient algorithm is known to solve it.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The full picture: private key → public key → address
&lt;/h2&gt;

&lt;p&gt;Let's tie it all together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The generator point G&lt;/strong&gt; is a specific point on the secp256k1 curve, hardcoded into the protocol. Everyone using Bitcoin uses the exact same G. It's not secret - it's published in the spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your private key k&lt;/strong&gt; is a random 256-bit integer you (or your wallet) generates. It's the secret. Guard it with your life and then also your death.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your public key&lt;/strong&gt; is computed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public Key = k × G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;×&lt;/code&gt; is scalar multiplication over the elliptic curve, operating in the finite field GF(p).&lt;/p&gt;

&lt;p&gt;Knowing &lt;code&gt;G&lt;/code&gt; and the public key, recovering &lt;code&gt;k&lt;/code&gt; requires solving ECDLP - which is computationally infeasible with current (and foreseeable) technology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your address&lt;/strong&gt; is derived from the public key through hashing (SHA-256 then RIPEMD-160 on Bitcoin, Keccak-256 on Ethereum), plus some encoding. Hashing is a separate one-way function layered on top - addresses are shorter than public keys, and even if some theoretical attack broke ECDLP (the Elliptic Curve Discrete Logarithm Problem described above), the hash layer provides an extra barrier.&lt;/p&gt;

&lt;p&gt;The full chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Random number k (secret)
      ↓  scalar multiplication (one-way)
Public Key (shareable)
      ↓  hashing (one-way)
Address (shareable)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each arrow is a one-way function. None can be reversed. That's the whole system.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next: digital signatures
&lt;/h2&gt;

&lt;p&gt;Everything above is the foundation for understanding &lt;strong&gt;digital signatures&lt;/strong&gt; - a critical part of how blockchains actually work. Every transaction you send is signed with your private key, and anyone can verify that signature using your public key without ever learning the key itself.&lt;/p&gt;

&lt;p&gt;The next post will explain how that signing process works on top of the elliptic curve math we covered here.&lt;/p&gt;




</description>
      <category>cryptography</category>
      <category>blockchain</category>
      <category>security</category>
      <category>math</category>
    </item>
    <item>
      <title>Hello Noir! [Part 2]</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Tue, 07 Apr 2026 19:29:00 +0000</pubDate>
      <link>https://dev.to/0xluk3/hello-noir-part-2-36l7</link>
      <guid>https://dev.to/0xluk3/hello-noir-part-2-36l7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In &lt;a href="https://dev.to/0xluk3/hello-noir-part-1-4m18"&gt;Part 1&lt;/a&gt; we wrote a circuit, compiled it with nargo, and got two artifacts: a circuit definition and a witness. Now we will use them to actually perform the cryptographic proof check.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Where we left off
&lt;/h2&gt;

&lt;p&gt;At the end of Part 1, we had two files in &lt;code&gt;target/&lt;/code&gt;:&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 bytecode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hello_world.gz&lt;/code&gt;&lt;/strong&gt; - the witness (our specific input values that satisfy the constraints)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All nargo told us was "yes, these inputs work." That's not a proof anyone else can verify - it's just a local check. To produce an actual cryptographic proof, we need Barretenberg (&lt;code&gt;bb&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Generating a proof with Barretenberg
&lt;/h2&gt;

&lt;p&gt;With our &lt;code&gt;Prover.toml&lt;/code&gt; set to &lt;code&gt;x = "2"&lt;/code&gt; and &lt;code&gt;y = "1"&lt;/code&gt; (recall: &lt;code&gt;x&lt;/code&gt; is private, &lt;code&gt;y&lt;/code&gt; is public), we run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bb prove &lt;span class="nt"&gt;-b&lt;/span&gt; ./target/hello_world.json &lt;span class="nt"&gt;-w&lt;/span&gt; ./target/hello_world.gz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--write_vk&lt;/span&gt; &lt;span class="nt"&gt;--verifier_target&lt;/span&gt; evm &lt;span class="nt"&gt;-o&lt;/span&gt; ./target
&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;Scheme is: ultra_honk, num threads: 8 (mem: 8.10 MiB)
CircuitProve: Proving key computed in 29 ms (mem: 24.21 MiB)
Public inputs saved to "./target/public_inputs" (mem: 28.56 MiB)
Proof saved to "./target/proof" (mem: 28.56 MiB)
VK saved to "./target/vk" (mem: 28.56 MiB)
VK Hash saved to "./target/vk_hash" (mem: 28.56 MiB)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down the flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-b&lt;/code&gt; - path to the compiled circuit (ACIR bytecode)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-w&lt;/code&gt; - path to the witness&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--write_vk&lt;/code&gt; - also generate the verification key alongside the proof&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--verifier_target evm&lt;/code&gt; - target the EVM. This does two things: uses Keccak256 (the EVM has a dedicated opcode for it, making verification gas-efficient) and enables the zero-knowledge property (the proof reveals nothing about private inputs)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-o&lt;/code&gt; - output directory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This produced four files in &lt;code&gt;target/&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;File&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;proof&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7,488 bytes&lt;/td&gt;
&lt;td&gt;The cryptographic proof&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,888 bytes&lt;/td&gt;
&lt;td&gt;Verification key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vk_hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32 bytes&lt;/td&gt;
&lt;td&gt;Hash of the vk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public_inputs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32 bytes&lt;/td&gt;
&lt;td&gt;The public inputs (y = 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What is the verification key?&lt;/strong&gt; It's derived from the circuit structure alone - not from your inputs. It encodes the circuit's "shape": how many gates, what type, how they're wired. Anyone can use it to verify proofs for this circuit. Think of it as the circuit's public fingerprint. The same vk works for any valid proof of this circuit, regardless of what specific values of &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; were used.&lt;/p&gt;

&lt;p&gt;One detail worth noting: &lt;code&gt;bb&lt;/code&gt; prepends the public inputs to the proof blob. The verifier contract knows to extract them from there.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Verifying the proof
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bb verify &lt;span class="nt"&gt;-p&lt;/span&gt; ./target/proof &lt;span class="nt"&gt;-k&lt;/span&gt; ./target/vk &lt;span class="nt"&gt;--verifier_target&lt;/span&gt; evm
&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;Scheme is: ultra_honk, num threads: 8 (mem: 8.11 MiB)
Proof verified successfully (mem: 18.36 MiB)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what we did NOT pass: the witness. That would defeat the purpose, wouldn't it? The whole point is that the verifier never sees your private inputs. All it needs is the proof and the verification key.&lt;/p&gt;

&lt;p&gt;The proof says: "someone knows a value &lt;code&gt;x&lt;/code&gt; such that &lt;code&gt;x != y&lt;/code&gt;, where &lt;code&gt;y = 1&lt;/code&gt;." It doesn't say what &lt;code&gt;x&lt;/code&gt; is.&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%2Fmxqxe42seafldb5ypoq5.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%2Fmxqxe42seafldb5ypoq5.png" alt="Proving Flow" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Generating a Solidity verifier
&lt;/h2&gt;

&lt;p&gt;Now we want this verification to happen on-chain. &lt;a href="https://barretenberg.aztec.network/docs/getting_started/" rel="noopener noreferrer"&gt;&lt;code&gt;bb&lt;/code&gt;&lt;/a&gt; can generate a Solidity contract that does exactly the same check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bb write_solidity_verifier &lt;span class="nt"&gt;-k&lt;/span&gt; ./target/vk &lt;span class="nt"&gt;--verifier_target&lt;/span&gt; evm &lt;span class="nt"&gt;-o&lt;/span&gt; ./target/Verifier.sol
&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;Scheme is: ultra_honk, num threads: 8 (mem: 8.75 MiB)
ZK Honk solidity verifier saved to "./target/Verifier.sol" (mem: 9.87 MiB)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a 2,449-line Solidity file. What's inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;HonkVerifier&lt;/code&gt; contract that inherits from &lt;code&gt;BaseZKHonkVerifier&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The verification key hardcoded as constants (circuit size, elliptic curve points, etc.)&lt;/li&gt;
&lt;li&gt;Pairing check logic using EVM precompiles (&lt;code&gt;ecAdd&lt;/code&gt;, &lt;code&gt;ecMul&lt;/code&gt;, &lt;code&gt;ecPairing&lt;/code&gt;, &lt;code&gt;modexp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A single entry point: &lt;code&gt;function verify(bytes calldata proof, bytes32[] calldata publicInputs) external returns (bool)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works on any EVM chain that supports the required precompiles - Ethereum mainnet, most L2s, and testnets.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Deploying with Foundry
&lt;/h2&gt;

&lt;p&gt;Let's deploy the verifier and verify a proof on-chain. First, set up a Foundry project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;forge init verifier-deploy
&lt;span class="nb"&gt;cd &lt;/span&gt;verifier-deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the generated verifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; ../hello_world/target/Verifier.sol src/Verifier.sol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to allow Foundry to read our proof file. Add this to &lt;code&gt;foundry.toml&lt;/code&gt;:&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;fs_permissions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="py"&gt;access&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"../hello_world/target"&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity &amp;gt;=0.8.21;

import "forge-std/Script.sol";
import "../src/Verifier.sol";

contract DeployScript is Script {
    function run() external {
        vm.startBroadcast();
        HonkVerifier verifier = new HonkVerifier();
        console.log("HonkVerifier deployed to:", address(verifier));
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// script/Verify.s.sol
// SPDX-License-Identifier: MIT
pragma solidity &amp;gt;=0.8.21;

import "forge-std/Script.sol";
import "../src/Verifier.sol";

contract VerifyScript is Script {
    function run() external {
        bytes memory proofBytes = vm.readFileBinary("../hello_world/target/proof");

        bytes32[] memory publicInputs = new bytes32[](1);
        publicInputs[0] = bytes32(uint256(1)); // y = 1

        vm.startBroadcast();
        HonkVerifier verifier = new HonkVerifier();
        console.log("HonkVerifier deployed to:", address(verifier));

        bool result = verifier.verify(proofBytes, publicInputs);
        console.log("Proof verified:", result);
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running it
&lt;/h3&gt;

&lt;p&gt;Start a local testnet and deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;anvil &lt;span class="nt"&gt;--code-size-limit&lt;/span&gt; 50000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--code-size-limit&lt;/code&gt; flag is needed because &lt;code&gt;HonkVerifier&lt;/code&gt; exceeds the default EIP-170 contract size limit of 24,576 bytes (ours is ~33K). This is fine for local testing. For production, you'd use &lt;code&gt;--optimized&lt;/code&gt; when generating the Solidity verifier or split the contract into libraries.&lt;/p&gt;

&lt;p&gt;In another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;forge script script/Verify.s.sol &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rpc-url&lt;/span&gt; http://127.0.0.1:8545 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-key&lt;/span&gt; 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--broadcast&lt;/span&gt; &lt;span class="nt"&gt;--code-size-limit&lt;/span&gt; 50000
&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;Script ran successfully.

== Logs ==
  HonkVerifier deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
  Proof verified: true

ONCHAIN EXECUTION COMPLETE &amp;amp; SUCCESSFUL.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proof verified on-chain. The same proof that &lt;code&gt;bb&lt;/code&gt; verified locally now passes through a Solidity contract running on an EVM.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Trust and threat model
&lt;/h2&gt;

&lt;p&gt;Before you ship anything, consider three attack surfaces.&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%2Fifho4y4a9ny1nbpojpan.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%2Fifho4y4a9ny1nbpojpan.png" alt="Trust Model" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prover honesty.&lt;/strong&gt; The circuit enforces constraints, not truth. Nobody can prove that &lt;code&gt;2 != 2&lt;/code&gt; - the constraint system rejects it. But nothing stops someone from proving &lt;code&gt;2000 != 18&lt;/code&gt; and claiming that proves their age. The circuit guarantees mathematical correctness of the relationship between inputs, not that the inputs themselves are meaningful. External anchoring (signed attestations, on-chain data, oracles) is required to bind proof inputs to real-world facts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;bb&lt;/code&gt; binary.&lt;/strong&gt; &lt;code&gt;bb&lt;/code&gt; is a local executable. If someone swaps it for a modified version, they could generate proofs that pass a compromised verifier. In any system where the prover and verifier are different entities, the verifier must run its own trusted copy of &lt;code&gt;bb&lt;/code&gt; (or verify on-chain where the contract is the trust anchor).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The verifier contract.&lt;/strong&gt; It's source code - editable before deployment. If the same entity generates the proof and deploys the verifier, there's a circular trust problem. For production, the verifier contract should be deployed by a trusted third party, verified on a block explorer, and ideally immutable (no proxy, no upgradeability). Or at the very least, governed by a multisig with a timelock.&lt;/p&gt;

&lt;p&gt;None of these are flaws in the cryptography. These are system design questions that any production deployment needs to answer.&lt;/p&gt;




&lt;p&gt;We went from compiled artifacts to a verified on-chain proof. The circuit is trivial, but the pipeline is the same one you'd use for anything more complex - age verification, credential checks, private voting. The hard part isn't the tooling. It's designing the system around it.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://noir-lang.org/docs/getting_started/hello_noir" rel="noopener noreferrer"&gt;Noir docs - Proving backend&lt;/a&gt; - official getting started guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://barretenberg.aztec.network/docs/getting_started/" rel="noopener noreferrer"&gt;Barretenberg&lt;/a&gt; proving backend documentation&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>noir</category>
      <category>zeroknowledge</category>
      <category>blockchain</category>
      <category>security</category>
    </item>
    <item>
      <title>Hello Noir! [Part 1]</title>
      <dc:creator>0xluk3</dc:creator>
      <pubDate>Tue, 07 Apr 2026 07:28:57 +0000</pubDate>
      <link>https://dev.to/0xluk3/hello-noir-part-1-4m18</link>
      <guid>https://dev.to/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" width="800" height="222"&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 plaintext"&gt;&lt;code&gt;[hello_world] Circuit witness successfully solved
[hello_world] Witness saved to target/hello_world.gz
&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" width="800" height="311"&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;&lt;a href="https://dev.to/0xluk3/hello-noir-part-2-36l7"&gt;Part 2 - Generating and verifying proofs&lt;/a&gt;&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://dev.to/0xluk3/math-cheatsheet-before-you-deep-dive-into-zk-1bo6</link>
      <guid>https://dev.to/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>
