<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>LUK3.TECH Blog</title><description>Security research, development, and blockchain.</description><link>https://luk3.tech/</link><item><title>Web3 security and OPSEC checklist: beyond the smart contract audit</title><link>https://luk3.tech/blog/web3-security-opsec-checklist/</link><guid isPermaLink="true">https://luk3.tech/blog/web3-security-opsec-checklist/</guid><description>A practical threat model for a small web3 company. Smart contract audits are just the start - here&apos;s everything else that can go wrong.</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&gt; RandomExampleFinance has been audited. Smart contracts - clean. And yet, six months later, funds are gone. This post is about everything the audit didn&apos;t cover.

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.

---

## The overview

Let&apos;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 `BYOD` and there is no expensive corporate `EDR` connected to a 24/7 `SOC` - sorry, we&apos;re on budget! Company&apos;s main product is a DeFi that allows some novel way to earn yield - it doesn&apos;t really matter how.

## The assets

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.

## How the funds could be stolen?

There are few common scenarios of attacks:

1. [A smart contract exploit leading to loss of funds](#1-a-smart-contract-exploit-leading-to-loss-of-funds)
2. [A web2 type exploit leading to loss of funds (no user interaction)](#2-a-web2-type-exploit-leading-to-loss-of-funds-no-user-interaction)
3. [A web2 type exploit leading to loss of funds (user interaction)](#3-a-web2-type-exploit-leading-to-loss-of-funds-user-interaction)
4. [Exploit or attack against exact person that has access to the funds (keys)](#4-exploit-or-attack-against-exact-person-that-has-access-to-the-funds-keys)
5. [Dependency confusion / Supply chain attack](#5-dependency-confusion-and-supply-chain)
6. [Governance/multisig or malicious employee attack](#6-governancemultisig-or-malicious-employee-attack)

Each of them has its different reasons and different way to prevent it.

---

## 1. A smart contract exploit leading to loss of funds

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;div class=&quot;callout-remediation&quot;&gt;

**Remediation:** smart contract audit, running a bug bounty program to support ongoing testing - one-time test doesn&apos;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](#audit-cycle) to maximize audit efficiency.

&lt;/div&gt;

&lt;div class=&quot;callout-tip&quot;&gt;

**Pro tip:** Companies tend to believe that they may change just one line of code vs audited commit and it shouldn&apos;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;/div&gt;

## 2. A web2 type exploit leading to loss of funds (no user interaction)

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.

A web2 exploit is something [OWASP WSTG](https://owasp.org/www-project-web-security-testing-guide/) describes. You probably heard of `SQL Injection` or `Remote Code Execution`. So the OWASP testing guide has 400+ pages and there are multiple exploit chains possible.

Even if the dApp is only a frontend, it can be abused - but this will be covered in the later point.

This depends on your attack surface - here are the key questions to ask:

&lt;div style=&quot;margin: 2rem 0; padding: 1.5rem; background: #161B22; border: 1px solid #30363D; border-radius: 8px;&quot;&gt;
  &lt;div style=&quot;text-align: center; margin-bottom: 1.25rem;&quot;&gt;
    &lt;span style=&quot;font-size: 1.1rem; margin-right: 0.4rem;&quot;&gt;&amp;#128737;&lt;/span&gt;
    &lt;span style=&quot;font-family: var(--font-mono); font-size: 0.75rem; color: #8B949E; text-transform: uppercase; letter-spacing: 0.1em;&quot;&gt;Example Web2 Attack Vectors&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem;&quot;&gt;
    &lt;div style=&quot;background: rgba(249, 115, 22, 0.08); border-left: 3px solid #f97316; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#9888; Orphaned Infrastructure&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Do you know your infrastructure? Orphaned servers, dangling &lt;code&gt;CNAME&lt;/code&gt;s leading to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Subdomain_takeover&quot; style=&quot;color: #58a6ff;&quot;&gt;subdomain takeover&lt;/a&gt;, old &lt;code&gt;VNC&lt;/code&gt; / &lt;code&gt;CMS&lt;/code&gt; on dev servers.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(249, 115, 22, 0.08); border-left: 3px solid #f97316; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#129302; AI Agent Exposure&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Do you use AI agents prone to &lt;code&gt;prompt injection&lt;/code&gt; on a machine with meaningful access or data?&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(249, 115, 22, 0.08); border-left: 3px solid #f97316; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128737; WAF &amp;amp; Rate Limiting&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Do you use minimum a Cloudflare &lt;code&gt;WAF&lt;/code&gt;? It strongly discourages enumerating and testing against known vulnerabilities.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(249, 115, 22, 0.08); border-left: 3px solid #f97316; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128274; Firewall &amp;amp; Ports&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Do you have a firewall blocking access to services? Can someone connect to exposed &lt;code&gt;SMTP&lt;/code&gt; &lt;code&gt;port 25&lt;/code&gt; and send emails on behalf of your domain?&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(249, 115, 22, 0.08); border-left: 3px solid #f97316; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128269; Scoped Pentest&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;A pentest of your infrastructure and core apps helps - but scope properly. Not just &quot;test the most popular app&quot; - first think: what are our assets and how do we protect them?&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(249, 115, 22, 0.08); border-left: 3px solid #f97316; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128273; Secrets Management&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Rotate on every exposure, no plaintext. Check repos, commits, and JS for hardcoded secrets - a leaked API key can mean full service takeover. Use &lt;a href=&quot;https://github.com/trufflesecurity/trufflehog&quot;&gt;&lt;code&gt;trufflehog&lt;/code&gt;&lt;/a&gt;.&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;callout-remediation&quot;&gt;

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

&lt;/div&gt;

&lt;div class=&quot;callout-tip&quot;&gt;

**Pro tip:** Scope your pentest around your actual assets, not your most popular app. &quot;Test everything&quot; with no asset inventory just burns budget on the wrong things.

&lt;/div&gt;

## 3. A web2 type exploit leading to loss of funds (user interaction)

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&apos;t always difficult to convince users, so the &quot;user interaction&quot; part should never be underestimated.

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?

Typical technical controls to help it are well-known frontend hardening measures such as:

- [SRI (Subresource Integrity)](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)
- [CSP (Content Security Policy)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
- [Cookie flags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security)
- [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)

All of them secure the user browser instructing it on what to load. This way, anything injected, like scripts, won&apos;t be effective. And the truth is, that every penetration test flags those - typically as &quot;low&quot; 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 &quot;official&quot; `CVSS` score for those, in web3 injections of scripts lead to great damage, hence those controls should be prioritized.

&lt;div class=&quot;callout-remediation&quot;&gt;

**Remediation:** SRI, CSP, proper cookie flags. Don&apos;t pull latest dependencies immediately on release. Treat third-party scripts as untrusted by default.

&lt;/div&gt;

&lt;div class=&quot;callout-tip&quot;&gt;

**Pro tip:** 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;/div&gt;

## 4. Exploit or attack against exact person that has access to the funds (keys)

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?

Typically when an automated [RAT](https://en.wikipedia.org/wiki/Remote_access_trojan) 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 `AV` detections, too.

Also think that the phishing may not always be a `RAT` 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.

![AWS phishing example](https://www.datocms-assets.com/75231/1723123584-awsphishin.png?fm=webp)
*Source: wiz.io/blog/emerging-phishing-campaign-targeting-aws-accounts*

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

&lt;div class=&quot;callout-remediation&quot;&gt;

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

&lt;/div&gt;

&lt;div class=&quot;callout-tip&quot;&gt;

**Pro tip:** If your lead dev has the production key on his laptop because he was &quot;just testing deployment&quot; - that&apos;s your biggest vulnerability right now, and no audit will catch it.

&lt;/div&gt;

## 5. Dependency confusion and Supply Chain

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&apos;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&apos;s the worst that can happen if external integrations misbehave?

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;div class=&quot;callout-remediation&quot;&gt;

**Remediation:** 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;/div&gt;

&lt;div class=&quot;callout-tip&quot;&gt;

**Pro tip:** The &quot;we&apos;ll just use latest&quot; policy is a bet that every maintainer of every package in your dependency tree will never get compromised. That&apos;s a lot of trust in strangers.

&lt;/div&gt;

## 6. Governance/multisig or malicious employee attack

That&apos;s something that&apos;s not rarely seen. Especially in remote, distributed teams, you don&apos;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?

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

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&apos;t remember. But there can be 1%.

&lt;div class=&quot;callout-remediation&quot;&gt;

**Remediation:** 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;/div&gt;

&lt;div class=&quot;callout-tip&quot;&gt;

**Pro tip:** 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;/div&gt;

---

## The practical thing: If I were to be a small company CISO, where would I invest?

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;div id=&quot;audit-cycle&quot;&gt;&lt;/div&gt;

### Follow the money - start with the smart contracts / web3 code

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 `onlyOwner` modifiers on admin functions.

&lt;div style=&quot;margin: 2rem auto; max-width: 520px;&quot;&gt;
  &lt;div style=&quot;background: rgba(78, 205, 196, 0.08); border-left: 3px solid #4ECDC4; padding: 1rem 1.25rem; border-radius: 0 6px 6px 0;&quot;&gt;
    &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.9rem; margin-bottom: 0.25rem;&quot;&gt;1 - Security-Oriented Design&lt;/div&gt;
    &lt;div style=&quot;font-size: 0.8rem; color: #8B949E;&quot;&gt;Think adversarially. Create threat model. Write extensive unit tests. Measure coverage.&lt;/div&gt;
  &lt;/div&gt;
  &lt;div style=&quot;text-align: center; color: #4ECDC4; font-size: 1.2rem; line-height: 1.5;&quot;&gt;&amp;#9660;&lt;/div&gt;
  &lt;div style=&quot;background: rgba(96, 165, 250, 0.08); border-left: 3px solid #60a5fa; padding: 1rem 1.25rem; border-radius: 0 6px 6px 0;&quot;&gt;
    &lt;div style=&quot;font-weight: 600; color: #60a5fa; font-size: 0.9rem; margin-bottom: 0.25rem;&quot;&gt;2 - First Review: Low-Hanging Fruits&lt;/div&gt;
    &lt;div style=&quot;font-size: 0.8rem; color: #8B949E;&quot;&gt;AI-assisted review (Claude Code, Codex, chatbot). Open source audit tools from trusted companies.&lt;/div&gt;
  &lt;/div&gt;
  &lt;div style=&quot;text-align: center; color: #60a5fa; font-size: 1.2rem; line-height: 1.5;&quot;&gt;&amp;#9660;&lt;/div&gt;
  &lt;div style=&quot;background: rgba(249, 115, 22, 0.08); border-left: 3px solid #f97316; padding: 1rem 1.25rem; border-radius: 0 6px 6px 0;&quot;&gt;
    &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.9rem; margin-bottom: 0.25rem;&quot;&gt;3 - Private Audit&lt;/div&gt;
    &lt;div style=&quot;font-size: 0.8rem; color: #8B949E;&quot;&gt;Smaller audit shop or qualified independent security researcher. Lower rates vs established names.&lt;/div&gt;
  &lt;/div&gt;
  &lt;div style=&quot;text-align: center; color: #f97316; font-size: 1.2rem; line-height: 1.5;&quot;&gt;&amp;#9660;&lt;/div&gt;
  &lt;div style=&quot;background: rgba(63, 185, 80, 0.08); border-left: 3px solid #3FB950; padding: 1rem 1.25rem; border-radius: 0 6px 6px 0;&quot;&gt;
    &lt;div style=&quot;font-weight: 600; color: #3FB950; font-size: 0.9rem; margin-bottom: 0.25rem;&quot;&gt;4 - Established Audit Firm / Top Researcher&lt;/div&gt;
    &lt;div style=&quot;font-size: 0.8rem; color: #8B949E;&quot;&gt;Final expensive pass. Auditors focus on edge cases and complex attack vectors.&lt;/div&gt;
  &lt;/div&gt;
  &lt;div style=&quot;text-align: center; color: #3FB950; font-size: 1.2rem; line-height: 1.5;&quot;&gt;&amp;#9660;&lt;/div&gt;
  &lt;div style=&quot;background: rgba(168, 85, 247, 0.08); border-left: 3px solid #a855f7; padding: 1rem 1.25rem; border-radius: 0 6px 6px 0;&quot;&gt;
    &lt;div style=&quot;font-weight: 600; color: #a855f7; font-size: 0.9rem; margin-bottom: 0.25rem;&quot;&gt;5 - (Optional) Bug Bounty / Security Contact&lt;/div&gt;
    &lt;div style=&quot;font-size: 0.8rem; color: #8B949E;&quot;&gt;Host a bug bounty program for ongoing researcher engagement. If not feasible, at minimum publish a &lt;a href=&quot;https://en.wikipedia.org/wiki/Security.txt&quot; target=&quot;_blank&quot;&gt;security.txt&lt;/a&gt; with a contact - responsible disclosure depends on researchers being able to reach you.&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;callout-tip&quot;&gt;

**Pro tip:** 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;/div&gt;

### Asset inventory and principle of least privilege

For any other infrastructure you have, especially web servers, websites, check the following things:

&lt;div style=&quot;margin: 2rem 0; padding: 1.5rem; background: #161B22; border: 1px solid #30363D; border-radius: 8px;&quot;&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem;&quot;&gt;
    &lt;div style=&quot;background: rgba(78, 205, 196, 0.08); border-left: 3px solid #4ECDC4; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128737; Internet-Facing Assets&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Enable Cloudflare, disable direct IP access, basic &lt;code&gt;WAF&lt;/code&gt; + rate limiting. Not a silver bullet but discourages most attackers from poking.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(78, 205, 196, 0.08); border-left: 3px solid #4ECDC4; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128269; OSINT / Blackbox Review&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Let security testers find all subdomains, servers, cloud assets, accounts, orphaned sites. Surprising discoveries guaranteed.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(78, 205, 196, 0.08); border-left: 3px solid #4ECDC4; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128270; White-Box Security Review&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Code audit + dynamic testing is most efficient. Don&apos;t rely on pure web2 firms - they may miss blockchain context. Close-source after, enable Cloudflare for production.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(78, 205, 196, 0.08); border-left: 3px solid #4ECDC4; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128683; Hide Non-Public Assets&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Whatever isn&apos;t explicitly client-facing should NOT be accessible. IP whitelisting or internal network. No worrying about old dev servers.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(78, 205, 196, 0.08); border-left: 3px solid #4ECDC4; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128274; Least Privilege&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Everything denied by default. Grant access only when required, revoke when no longer used. Admin console, internal network - all of it.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(78, 205, 196, 0.08); border-left: 3px solid #4ECDC4; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;&amp;#128273; Multi Factor Authentication&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;E.g. Google Authenticator. All user accounts must be protected with it, not only strong, complex password. Use password managers to enforce the latter.&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

{/* IMAGE: Screenshot of Shodan or Censys showing exposed services/ports */}

### Security culture

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?

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.

### Threat modeling

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 [STRIDE described by OWASP](https://owasp.org/www-community/Threat_Modeling_Process), 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;div style=&quot;margin: 2rem 0; padding: 1.5rem; background: #161B22; border: 1px solid #30363D; border-radius: 8px;&quot;&gt;
  &lt;div style=&quot;text-align: center; margin-bottom: 1.25rem;&quot;&gt;
    &lt;span style=&quot;font-family: var(--font-mono); font-size: 0.75rem; color: #8B949E; text-transform: uppercase; letter-spacing: 0.1em;&quot;&gt;STRIDE Threat Model&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem;&quot;&gt;
    &lt;div style=&quot;background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #ef4444; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;S - Spoofing&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Pretending to be someone else. Fake login pages, stolen credentials, forged tokens.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(249, 115, 22, 0.1); border-left: 3px solid #f97316; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #f97316; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;T - Tampering&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Modifying data or code. Altered transactions, changed configs, injected scripts.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(234, 179, 8, 0.1); border-left: 3px solid #eab308; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #eab308; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;R - Repudiation&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Denying actions taken. No audit logs, unsigned transactions, missing evidence.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(78, 205, 196, 0.1); border-left: 3px solid #4ECDC4; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #4ECDC4; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;I - Info Disclosure&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Exposing sensitive data. Leaked keys, verbose errors, exposed .env files.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(96, 165, 250, 0.1); border-left: 3px solid #60a5fa; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #60a5fa; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;D - Denial of Service&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Making things unavailable. DDoS, resource exhaustion, gas griefing.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background: rgba(168, 85, 247, 0.1); border-left: 3px solid #a855f7; padding: 0.75rem; border-radius: 0 6px 6px 0;&quot;&gt;
      &lt;div style=&quot;font-weight: 600; color: #a855f7; font-size: 0.85rem; margin-bottom: 0.25rem;&quot;&gt;E - Elevation of Privilege&lt;/div&gt;
      &lt;div style=&quot;font-size: 0.75rem; color: #8B949E;&quot;&gt;Gaining unauthorized access. Missing access controls, privilege escalation, admin bypass.&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&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 [die to a coconut](https://en.wikipedia.org/wiki/Death_by_coconut). 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.

But if there&apos;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&apos;t the code - it&apos;s a process someone skipped because it was inconvenient that day.</content:encoded></item><item><title>Hello Noir! [Part 2]</title><link>https://luk3.tech/blog/hello-noir-part-2/</link><guid isPermaLink="true">https://luk3.tech/blog/hello-noir-part-2/</guid><description>Using Barretenberg to generate and verify proofs, deploying a Solidity verifier with Foundry, and understanding the trust model.</description><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&gt; In [Part 1](/blog/hello-noir-part-1) 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.

---

## 1. Where we left off

At the end of Part 1, we had two files in `target/`:

- **`hello_world.json`** - the compiled circuit (ACIR bytecode)
- **`hello_world.gz`** - the witness (our specific input values that satisfy the constraints)

All nargo told us was &quot;yes, these inputs work.&quot; That&apos;s not a proof anyone else can verify - it&apos;s just a local check. To produce an actual cryptographic proof, we need Barretenberg (`bb`).

---

## 2. Generating a proof with Barretenberg

With our `Prover.toml` set to `x = &quot;2&quot;` and `y = &quot;1&quot;` (recall: `x` is private, `y` is public), we run:

```bash
bb prove -b ./target/hello_world.json -w ./target/hello_world.gz \
  --write_vk --verifier_target evm -o ./target
```

```
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 &quot;./target/public_inputs&quot; (mem: 28.56 MiB)
Proof saved to &quot;./target/proof&quot; (mem: 28.56 MiB)
VK saved to &quot;./target/vk&quot; (mem: 28.56 MiB)
VK Hash saved to &quot;./target/vk_hash&quot; (mem: 28.56 MiB)
```

Let&apos;s break down the flags:

- `-b` - path to the compiled circuit (ACIR bytecode)
- `-w` - path to the witness
- `--write_vk` - also generate the verification key alongside the proof
- `--verifier_target evm` - 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)
- `-o` - output directory

This produced four files in `target/`:

&lt;table style=&quot;width:100%; border-collapse:collapse; margin:1rem 0;&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;border-bottom:2px solid #30363D;&quot;&gt;
&lt;th style=&quot;text-align:left; padding:0.5rem; color:#C9D1D9;&quot;&gt;File&lt;/th&gt;
&lt;th style=&quot;text-align:left; padding:0.5rem; color:#C9D1D9;&quot;&gt;Size&lt;/th&gt;
&lt;th style=&quot;text-align:left; padding:0.5rem; color:#C9D1D9;&quot;&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;border-bottom:1px solid #21262D;&quot;&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;&lt;code&gt;proof&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;7,488 bytes&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;The cryptographic proof&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-bottom:1px solid #21262D;&quot;&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;&lt;code&gt;vk&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;1,888 bytes&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;Verification key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-bottom:1px solid #21262D;&quot;&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;&lt;code&gt;vk_hash&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;32 bytes&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;Hash of the vk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-bottom:1px solid #21262D;&quot;&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;&lt;code&gt;public_inputs&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;32 bytes&lt;/td&gt;
&lt;td style=&quot;padding:0.5rem;&quot;&gt;The public inputs (y = 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

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

One detail worth noting: `bb` prepends the public inputs to the proof blob. The verifier contract knows to extract them from there.

---

## 3. Verifying the proof

```bash
bb verify -p ./target/proof -k ./target/vk --verifier_target evm
```

```
Scheme is: ultra_honk, num threads: 8 (mem: 8.11 MiB)
Proof verified successfully (mem: 18.36 MiB)
```

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

The proof says: &quot;someone knows a value `x` such that `x != y`, where `y = 1`.&quot; It doesn&apos;t say what `x` is.

![Proving Flow](/blog/noir-proving-flow.svg)

---

## 4. Generating a Solidity verifier

Now we want this verification to happen on-chain. [`bb`](https://barretenberg.aztec.network/docs/getting_started/) can generate a Solidity contract that does exactly the same check:

```bash
bb write_solidity_verifier -k ./target/vk --verifier_target evm -o ./target/Verifier.sol
```

```
Scheme is: ultra_honk, num threads: 8 (mem: 8.75 MiB)
ZK Honk solidity verifier saved to &quot;./target/Verifier.sol&quot; (mem: 9.87 MiB)
```

The result is a 2,449-line Solidity file. What&apos;s inside:

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

This works on any EVM chain that supports the required precompiles - Ethereum mainnet, most L2s, and testnets.

---

## 5. Deploying with Foundry

Let&apos;s deploy the verifier and verify a proof on-chain. First, set up a Foundry project:

```bash
forge init verifier-deploy
cd verifier-deploy
```

Copy the generated verifier:

```bash
cp ../hello_world/target/Verifier.sol src/Verifier.sol
```

We also need to allow Foundry to read our proof file. Add this to `foundry.toml`:

```toml
fs_permissions = [{ access = &quot;read&quot;, path = &quot;../hello_world/target&quot; }]
```

### Deploy script

```solidity
// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.8.21;

import &quot;forge-std/Script.sol&quot;;
import &quot;../src/Verifier.sol&quot;;

contract DeployScript is Script {
    function run() external {
        vm.startBroadcast();
        HonkVerifier verifier = new HonkVerifier();
        console.log(&quot;HonkVerifier deployed to:&quot;, address(verifier));
        vm.stopBroadcast();
    }
}
```

### Verify script

```solidity
// script/Verify.s.sol
// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.8.21;

import &quot;forge-std/Script.sol&quot;;
import &quot;../src/Verifier.sol&quot;;

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

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

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

        bool result = verifier.verify(proofBytes, publicInputs);
        console.log(&quot;Proof verified:&quot;, result);
        vm.stopBroadcast();
    }
}
```

### Running it

Start a local testnet and deploy:

```bash
anvil --code-size-limit 50000
```

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

In another terminal:

```bash
forge script script/Verify.s.sol \
  --rpc-url http://127.0.0.1:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  --broadcast --code-size-limit 50000
```

```
Script ran successfully.

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

ONCHAIN EXECUTION COMPLETE &amp; SUCCESSFUL.
```

The proof verified on-chain. The same proof that `bb` verified locally now passes through a Solidity contract running on an EVM.

---

## 6. Trust and threat model

Before you ship anything, consider three attack surfaces.

![Trust Model](/blog/noir-trust-model.svg)

**Prover honesty.** The circuit enforces constraints, not truth. Nobody can prove that `2 != 2` - the constraint system rejects it. But nothing stops someone from proving `2000 != 18` 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.

**The `bb` binary.** `bb` 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 `bb` (or verify on-chain where the contract is the trust anchor).

**The verifier contract.** It&apos;s source code - editable before deployment. If the same entity generates the proof and deploys the verifier, there&apos;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.

None of these are flaws in the cryptography. These are system design questions that any production deployment needs to answer.

---

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

---

## Recommended reading

- [Noir docs - Proving backend](https://noir-lang.org/docs/getting_started/hello_noir) - official getting started guide
- [Barretenberg](https://barretenberg.aztec.network/docs/getting_started/) - proving backend documentation
- [Foundry Book](https://book.getfoundry.sh/) - Solidity development framework
- [EVM precompiles](https://www.evm.codes/precompiled) - the precompiled contracts that make on-chain verification possible</content:encoded></item><item><title>Hello Noir! [Part 1]</title><link>https://luk3.tech/blog/hello-noir-part-1/</link><guid isPermaLink="true">https://luk3.tech/blog/hello-noir-part-1/</guid><description>Setting up the environment and getting an idea what and how will be used.</description><pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&gt; ZK is a hot topic. But what does it even mean to build a ZK circuit? Let&apos;s build a barebone, super basic circuit so you can have a better understanding what its part of.

---

## 1. What we&apos;re building and the toolchain

What we&apos;re building through this series is a SNARK - a Succinct Non-interactive Argument of Knowledge. Barretenberg, the proving backend we&apos;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 `--zk` flag for that), so what we produce is strictly a SNARK, not necessarily a zkSNARK - but the ecosystem loosely calls everything &quot;zk&quot; since the tooling supports it.
Here&apos;s the high-level flow of what we&apos;re doing:

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

So we use these tools to:

- [Noir](https://noir-lang.org/) - write the circuit (the constraints)
- [Barretenberg](https://barretenberg.aztec.network/docs/getting_started/) - generate proofs and verifier contracts
- [Foundry](https://book.getfoundry.sh/) - deploy and test on-chain

Now, why those? We should be doing Noir ZK right?

Yes, but Noir is only the language of constraints - it tells the system *what* to prove. We also need to generate a proof (this will be what users submit) and a component to check if it&apos;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&apos;t use Foundry in this post, but it shows up in Part 2.

This is a barebone implementation of a ZK app.

![Noir Toolchain Overview](/blog/noir-toolchain-overview.svg)

---

## 2. Setting up the environment

Noir&apos;s toolchain depends on [Rust](https://www.rust-lang.org/tools/install). If you don&apos;t have it:

```bash
curl --proto &apos;=https&apos; --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

Install [Noir via `noirup`](https://noir-lang.org/docs/getting_started/noir_installation):

```bash
curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
noirup
```

Verify with `nargo --version`.

Install [Barretenberg via `bbup`](https://barretenberg.aztec.network/docs/getting_started/):

```bash
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/master/barretenberg/bbup/install | bash
bbup
```

Verify with `bb --version`.

That&apos;s it. You&apos;re ready to write a circuit.

---

## 3. Writing a sample circuit

Let&apos;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.

```bash
nargo new hello_world
cd hello_world
```

```
Project successfully created! It is located at /home/dev/noir-hello-world/hello_world
```

This creates `Nargo.toml` (the project manifest, think `package.json` or `Cargo.toml`) and `src/main.nr` - your circuit.

```toml
[package]
name = &quot;hello_world&quot;
type = &quot;bin&quot;
authors = [&quot;&quot;]

[dependencies]
```

And the default `src/main.nr`:

```rust
fn main(x: u64, y: pub u64) {
    assert(x != y);
}

#[test]
fn test_main() {
    main(1, 2);

    // Uncomment to make test fail
    // main(1, 1);
}
```

Our circuit just checks if `x` is not equal to `y`. Simple. But notice the types - `x` is `u64` and `y` is `pub u64`. 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 `pub`.

So here, `x` is private and `y` is public. When a proof is generated, the verifier can see `y` but learns nothing about `x`. The proof only guarantees that *some* value of `x` exists that satisfies the constraint.

Here&apos;s a more intuitive example - imagine an age verification circuit:

```rust
fn main(age: u8, pub min_age: u8) {
    assert(age &gt;= min_age);
}
```

Here `min_age` is explicitly set to public. While we generate the proof, `age` is not revealed, while `min_age` is public - anyone can see you&apos;re checking against 18. The proof says &quot;this person is old enough&quot; without revealing whether they&apos;re 19 or 90.

Now, we can run:

```bash
nargo check
```

This validates your circuit and creates a `Prover.toml` file - a template for your inputs:

```toml
x = &quot;&quot;
y = &quot;&quot;
```

Let&apos;s input some values:

```toml
x = &quot;2&quot;
y = &quot;1&quot;
```

And run:

```bash
nargo execute
```

```
[hello_world] Circuit witness successfully solved
[hello_world] Witness saved to target/hello_world.gz
```

The circuit compiled, the inputs satisfied the constraint (`2 != 1`), and `nargo` saved the result.

And what if we tried to prove something that does not meet constraints? Change `Prover.toml` so both values are equal:

```toml
x = &quot;2&quot;
y = &quot;2&quot;
```

Run `nargo execute` again:

```
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: &apos;Cannot satisfy constraint&apos;
```

The inputs don&apos;t satisfy the rules. No witness is generated, no proof to produce.

---

## 4. What did nargo just produce

Look in the `target/` directory. You&apos;ll find two files:

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

These are **two different artifacts**, not compressed and uncompressed versions of the same thing.

![Compile and Execute Flow](/blog/noir-compile-flow.svg)

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

---

## 5. What&apos;s next

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

I don&apos;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&apos;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.

[Part 2 - Generating and verifying proofs](/blog/hello-noir-part-2)

---

## Recommended reading

- [Hot Chocolate](https://zkshark.notion.site/Hot-Chocolate-Beginners-guide-14907561ca1a80e68bd1d9245a53fd95) - beginner&apos;s guide to Noir
- [NoirJS app tutorial](https://noir-lang.org/docs/tutorials/noirjs_app) - official Noir docs
- [vkpatva/noir](https://github.com/vkpatva/noir) - example Noir circuits repo
- [Barretenberg](https://barretenberg.aztec.network/docs/getting_started/) - Aztec guide to Barretenberg</content:encoded></item><item><title>Lagrange interpolation: turning points into a polynomial</title><link>https://luk3.tech/blog/lagrange-interpolation/</link><guid isPermaLink="true">https://luk3.tech/blog/lagrange-interpolation/</guid><description>You have a list of points. Lagrange interpolation gives you the one polynomial that passes through all of them. Here&apos;s why ZK proofs care.</description><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>## 1. What is a polynomial?

A polynomial is an expression built from a variable $x$, using only addition, multiplication, and non-negative integer exponents:

$$P(x) = 3x^2 + 2x + 1$$

The **degree** is the highest power of $x$. The example above is degree 2 (quadratic) because $x^2$ is the highest term. Some more examples:

- $5x^3 - x$ - degree 3 (cubic)
- $7x + 4$ - degree 1 (linear)
- $42$ - degree 0 (constant)

&gt; Why does $7x + 4$ have no $x$ on the last term? Because $4$ is really $4 \cdot x^0$, and $x^0 = 1$ for any value of $x$. Every polynomial has an implicit $x^0$ on its constant term - we just don&apos;t write it.

The general form of a degree-$n$ polynomial:

$$P(x) = a_n x^n + a_{n-1} x^{n-1} + \cdots + a_1 x + a_0$$

where $a_0, a_1, \ldots, a_n$ are constants called **coefficients**.

---

## 2. The setup: points on a graph

You have three data points - expressed as inputs and outputs of some unknown function:

$$P(1) = 2, \quad P(2) = 5, \quad P(3) = 10$$

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 [unisolvence theorem](https://en.wikipedia.org/wiki/Polynomial_interpolation#Uniqueness_of_the_interpolating_polynomial) tells us that $n$ points pin down exactly one polynomial of degree at most $n - 1$:

- 1 point - exactly one degree-0 polynomial (a constant)
- 2 points - exactly one degree-1 polynomial (a line)
- 3 points - exactly one degree-2 polynomial (a parabola)

For our three points, that unique parabola is $P(x) = x^2 + 1$. No lower-degree polynomial (a line, a constant) can pass through all three - this is the lowest-degree polynomial that fits.

Finding that polynomial is called **interpolation**. One way to do it: set up a system of equations. Plug each point into $P(x) = ax^2 + bx + c$:

$$P(1): \quad a + b + c = 2$$

$$P(2): \quad 4a + 2b + c = 5$$

$$P(3): \quad 9a + 3b + c = 10$$

Subtract the first equation from the second: $3a + b = 3$. Subtract the second from the third: $5a + b = 5$. Subtract those two results: $2a = 2$, so $a = 1$. Back-substitute: $b = 0$, $c = 1$. The answer is $P(x) = x^2 + 1$.

It works, but it&apos;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.

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

---

## 3. Lagrange&apos;s approach: basis polynomials

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

### The goal: three basis polynomials

Our three x-values are $1, 2, 3$. Lagrange&apos;s idea: build three **basis polynomials**, one for each x-value. Each one equals 1 at its own x-value and 0 at the other two. We&apos;ll call them $L_1$, $L_2$, $L_3$.

**$L_1(x)$** - should equal 1 at $x = 1$, and zero at $x = 2$ and $x = 3$.

How do we build it? The expression $(x - 2)$ equals zero when $x = 2$. The expression $(x - 3)$ equals zero when $x = 3$. Multiply them together:

$$(x - 2)(x - 3)$$

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$?

$$(1 - 2)(1 - 3) = (-1)(-2) = 2$$

We got 2, but we need 1. So we divide the whole thing by 2:

$$L_1(x) = \frac{(x - 2)(x - 3)}{2}$$

Verify: $L_1(1) = \frac{(-1)(-2)}{2} = 1$, $\quad L_1(2) = \frac{(0)(-1)}{2} = 0$, $\quad L_1(3) = \frac{(1)(0)}{2} = 0$. Exactly what we need.

&gt; If you expand $\frac{(x-2)(x-3)}{2}$, you get $\frac{1}{2}x^2 - \frac{5}{2}x + 3$ - a polynomial just like in section 1. But the factored form is the point: it makes the zeros and the normalization obvious.

**$L_2(x)$** - should equal 1 at $x = 2$, and zero at $x = 1$ and $x = 3$.

Same recipe. We need zeros at $x = 1$ and $x = 3$, so use factors $(x - 1)(x - 3)$. Evaluate at $x = 2$: $(2-1)(2-3) = -1$. Divide by $-1$:

$$L_2(x) = \frac{(x - 1)(x - 3)}{-1}$$

- $L_2(1) = 0, \quad L_2(2) = 1, \quad L_2(3) = 0$

**$L_3(x)$** - should equal 1 at $x = 3$, and zero at $x = 1$ and $x = 2$.

Factors $(x - 1)(x - 2)$. Evaluate at $x = 3$: $(3-1)(3-2) = 2$. Divide by 2:

$$L_3(x) = \frac{(x - 1)(x - 2)}{2}$$

- $L_3(1) = 0, \quad L_3(2) = 0, \quad L_3(3) = 1$

Each polynomial equals 1 at one x-value and 0 at the others:

### Why 1 matters

The number 1 is a neutral multiplier: $1 \times \text{anything} = \text{anything}$. Since $L_1(1) = 1$, we can multiply by any y-value and deliver it exactly:

$$2 \cdot L_1(1) = 2 \cdot 1 = 2$$

And since $L_1$ is 0 at the other x-values, those stay zero:

$$2 \cdot L_1(2) = 2 \cdot 0 = 0 \qquad 2 \cdot L_1(3) = 2 \cdot 0 = 0$$

So $2 \cdot L_1(x)$ gives us exactly 2 at $x = 1$ and contributes nothing at the other points. Where does the 2 come from? It&apos;s the **y-value** of our first data point: $(1, \mathbf{2})$.

### Combining them

Do the same for each data point. Multiply each $L_i$ by the y-value of that point and add them up. The y-values come from our data points: $(1, \mathbf{2})$, $(2, \mathbf{5})$, $(3, \mathbf{10})$.

$$P(x) = 2 \cdot L_1(x) + 5 \cdot L_2(x) + 10 \cdot L_3(x)$$

Check at each point:

$$P(1) = 2 \cdot 1 + 5 \cdot 0 + 10 \cdot 0 = 2$$

$$P(2) = 2 \cdot 0 + 5 \cdot 1 + 10 \cdot 0 = 5$$

$$P(3) = 2 \cdot 0 + 5 \cdot 0 + 10 \cdot 1 = 10$$

All three data points are hit. Each $L_i$ delivers its y-value to the right place and contributes nothing elsewhere.

In general, for $n$ points the $i$-th basis polynomial is:

$$L_i(x) = \prod_{\substack{j=1 \\ j \neq i}}^{n} \frac{x - x_j}{x_i - x_j}$$

The $\prod$ symbol means &quot;multiply all these together&quot; - like a `for` loop that multiplies instead of adding. In pseudocode:

```
function basis(i, x, points):
    result = 1
    for j = 1 to n:
        if j != i:
            result = result * (x - points[j].x) / (points[i].x - points[j].x)
    return result
```

The loop skips $j = i$ and multiplies one fraction per remaining point. The numerator $(x - x_j)$ creates a zero at $x_j$; the denominator $(x_i - x_j)$ normalizes to 1 at $x_i$.

---

## 4. The interpolating polynomial

Section 3 built the result for three specific points. The general formula for any set of $n$ points:

$$P(x) = \sum_{i=1}^{n} y_i \cdot L_i(x)$$

The $\sum$ symbol means &quot;add all these together&quot; - like a `for` loop that adds. In pseudocode:

```
function interpolate(x, points):
    result = 0
    for i = 1 to n:
        result = result + points[i].y * basis(i, x, points)
    return result
```

The loop goes through each data point, computes its basis polynomial at $x$, multiplies by the y-value, and adds it to the total.

&gt; **This is Lagrange interpolation.** 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.

&gt; **Result:** From three basis polynomials $L_1$, $L_2$, $L_3$ and three data points $(1, 2)$, $(2, 5)$, $(3, 10)$, we combined them into a weighted sum:
&gt;
&gt; $$P(x) = 2 \cdot L_1(x) + 5 \cdot L_2(x) + 10 \cdot L_3(x) = x^2 + 1$$
&gt;
&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.

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.

---

## 5. Why ZK cares

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.

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&apos;s exactly one polynomial through any set of points is what makes this work - if the prover&apos;s polynomial passes through the right values, it must be the right polynomial.

---

## Further resources

- [Lagrange Interpolation Calculator](https://interpolationcalculator.com/lagrange-interpolation/) - plug in your own points and see the polynomial step by step
- [Polynomial interpolation - Wikipedia](https://en.wikipedia.org/wiki/Lagrange_polynomial)</content:encoded></item><item><title>The Fiat-Shamir transform: how a hash function replaces a conversation</title><link>https://luk3.tech/blog/fiat-shamir-transform/</link><guid isPermaLink="true">https://luk3.tech/blog/fiat-shamir-transform/</guid><description>Schnorr signatures started as a back-and-forth conversation. Fiat-Shamir turned that conversation into a one-liner. Here&apos;s how a hash function replaces a trusted stranger.</description><pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&gt; This post assumes you&apos;ve read the [Schnorr signatures post](/blog/digital-signatures-schnorr-ecdsa). You should be comfortable with `s = k + ex`, the verification equation `sG = R + eP`, and why nonce reuse is catastrophic. We&apos;re picking up exactly where that left off.

---

## 1. Schnorr as a conversation

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

Schnorr originally designed his scheme as an **identification protocol** - a way for Alice to prove to a server that she&apos;s the real account holder. Think of it like logging in, except instead of sending a password, Alice proves she knows the private key `x` 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:

1. **Alice → Bob:** Alice picks a random nonce `k`, computes `R = kG`, and sends `R` to Bob. This is her **commitment** - she&apos;s locked into a particular `k` without revealing it.
2. **Bob → Alice:** Bob picks a random challenge `e` and sends it back.
3. **Alice → Bob:** Alice computes `s = k + ex` and sends `s` to Bob. Bob verifies that `sG = R + eP`.

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

This protocol works. The question is: **can we take this conversation offline?**

On a blockchain, there is no Bob. There&apos;s no one to send a live challenge between steps 1 and 3. &quot;Bob&quot; is thousands of nodes verifying a transaction hours or days after it was created. There&apos;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.

---

## 2. The Fiat-Shamir heuristic: replace Bob with a hash

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

Instead of waiting for Bob to send a random `e`, Alice computes:

```
e = H(R || P || m)
```

Alice takes her commitment `R`, her public key `P`, and the message `m`, and hashes them together. The output is the challenge `e`. No Bob needed.

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

### What does verification actually prove?

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

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

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

Alice proved she owns the private key behind `P` without revealing it. That&apos;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 &quot;I know `x` such that `P = xG`.&quot; Remember that when we get to ZK-SNARKs, where the statement is an entire computation.

---

## 3. The signature: three steps collapsed into one

Let&apos;s write out the full Fiat-Shamir-ized Schnorr signature:

**Signing:**
1. Pick a random nonce `k`
2. Compute `R = kG`
3. Compute `e = H(R || P || m)`
4. Compute `s = k + ex (mod n)`
5. Publish the signature `(R, s)`

**Verification:**
1. Recompute `e = H(R || P || m)`
2. Check that `sG = R + eP`

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&apos;t name it. The &quot;challenge&quot; `e` that seemed like a straightforward hash was actually doing something profound: replacing an interactive protocol with a non-interactive one.

The three-message conversation (send `R`, receive `e`, send `s`) 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&apos;s no trusted verifier, no live interaction, just math and a hash function.

---

## 4. The Random Oracle: why this is secure (with a caveat)

The security proof for Fiat-Shamir relies on a model called the **Random Oracle Model** (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&apos;s random challenge, so the non-interactive scheme inherits the security of the interactive one.

SHA-256 isn&apos;t actually a Random Oracle. It&apos;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, &quot;good enough&quot; is a real thing - if the best known attack requires more computation than there are atoms in the universe, you&apos;re fine.

&lt;details&gt;
  &lt;summary&gt;ROM vs. Standard Model (optional deep dive)&lt;/summary&gt;

  Cryptographers distinguish between proofs in the **Random Oracle Model** and proofs in the **Standard Model** (which makes no idealized assumptions about hash functions).

  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&apos;t correspond to real-world schemes.

  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.

  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;/details&gt;

---

## 5. Why this matters: from conversations to blockchains

Fiat-Shamir gives us three things that make modern cryptographic systems work:

**Non-interactivity.** 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.

**Determinism.** Given the same inputs (`k`, `x`, `m`), 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.

**Security.** 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&apos;s no wiggle room.

And here&apos;s the thing that makes Fiat-Shamir more than a historical footnote: **it&apos;s everywhere**. 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.

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&apos;s random challenge with a hash of the prover&apos;s commitments.

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

---</content:encoded></item><item><title>Digital signatures: Schnorr, ECDSA and how PS3 was hacked</title><link>https://luk3.tech/blog/digital-signatures-schnorr-ecdsa/</link><guid isPermaLink="true">https://luk3.tech/blog/digital-signatures-schnorr-ecdsa/</guid><description>You made it through elliptic curves. Now let&apos;s see how they&apos;re actually used - signing things, proving identity, and why reusing a nonce will ruin your life.</description><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&gt; In the [previous post](/blog/private-keys-elliptic-curves), we built up to the key equation: `Public Key = x * G`. A private key `x`, a generator point `G`, and a public key `P` that anyone can see but nobody can reverse. This post picks up exactly where that left off.

---

## What is a digital signature?

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&apos;re actually secure.

A digital signature has three properties:

1. **Authentication** - it proves the signer has the private key
2. **Integrity** - it proves the message hasn&apos;t been tampered with
3. **Non-repudiation** - the signer can&apos;t later deny signing it

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.

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

Bear with me.

---

## 1. Schnorr signatures: the ingredients

Quick refresher from the previous post. We have:

- **G** - the generator point (public, same for everyone)
- **x** - the private key (secret, a scalar)
- **P = xG** - the public key (public, a point on the curve)

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

From `k`, we compute the **nonce commitment**:

```
R = kG
```

Just like the public key is `k` times `G`, the nonce commitment is `k` times `G`. Same operation, different secret. `R` is a point on the curve.

### Signing

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

**Step 1.** Pick a random nonce `k`, compute `R = kG`

**Step 2.** Compute the challenge:

```
e = H(R || P || m)
```

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

**Step 3.** Compute the signature scalar:

```
s = k + ex  (mod n)
```

where `n` is the order of the curve (the number of points in the group generated by `G`).

**The signature is the pair `(R, s)`.**

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

  You might wonder: if the challenge `e` depends on `R` (which the signer computes), can&apos;t the signer cheat by picking `e` first and then engineering `R` to match?

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

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

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

&lt;/details&gt;

---

## 2. Schnorr verification: why it works

The verifier has: the message `m`, the public key `P`, and the signature `(R, s)`.

**Step 1.** Recompute the challenge:

```
e = H(R || P || m)
```

**Step 2.** Check whether:

```
sG = R + eP
```

If both sides are equal, the signature is valid.

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

&lt;details&gt;
  &lt;summary&gt;Algebraic proof (expand if you want to see the substitution)&lt;/summary&gt;

  We know that `s = k + ex (mod n)`.

  Multiply both sides by `G`:

  ```
  sG = (k + ex)G
  sG = kG + exG
  sG = kG + e(xG)
  ```

  But `kG = R` (nonce commitment) and `xG = P` (public key), so:

  ```
  sG = R + eP
  ```

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

&lt;/details&gt;

### Why can&apos;t you fake it?

Three attack scenarios and why they all fail:

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

**Attack 2: Reuse someone else&apos;s signature.** The challenge `e` includes the message `m`. A different message produces a different `e`, which requires a different `s`. Old signatures don&apos;t work on new messages.

**Attack 3: Modify the message after signing.** Changing even one bit of `m` changes `e` (because hash functions), which breaks the equation `sG = R + eP`. Tampering is immediately detectable.

---

## 3. ECDSA: the one everyone uses

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

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&apos;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.

### ECDSA signing

Same ingredients: private key `x`, nonce `k`, message `m`. But the math is different.

**Step 1.** Pick random nonce `k`, compute `R = kG`. Let `r` be the x-coordinate of `R`.

**Step 2.** Compute:

```
s = k⁻¹(m + rx)  (mod n)
```

where `k⁻¹` is the inverse of `k` (the number that satisfies `k * k⁻¹ = 1 mod n` - think of it as division in modular arithmetic).

**The signature is `(r, s)`.**

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

### The r, s, and v of Ethereum

If you&apos;ve looked at Ethereum transactions, you&apos;ve seen three values: `v`, `r`, `s`.

- **r** - the x-coordinate of the nonce point `R = kG`
- **s** - the signature scalar from the formula above
- **v** - the recovery ID (27 or 28, or 0/1 in EIP-155)

An ECDSA signature is 65 bytes: 32 bytes for `r`, 32 bytes for `s`, and 1 byte for `v`.

### ecrecover: public key recovery

Here&apos;s something ECDSA can do that Schnorr can&apos;t: **recover the public key from the signature alone**.

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

Ethereum uses `ecrecover` heavily. Instead of storing the sender&apos;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.

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

---

## 4. Schnorr vs ECDSA

### Why Schnorr&apos;s linearity matters

The Schnorr equation `s = k + ex` is **linear** in both `k` and `x`. This has a powerful consequence: signatures can be **aggregated**.

If Alice signs with `s₁ = k₁ + ex₁` and Bob signs with `s₂ = k₂ + ex₂`, you can combine them:

```
s = s₁ + s₂ = (k₁ + k₂) + e(x₁ + x₂)
```

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

ECDSA can&apos;t do this. The `k⁻¹` term makes the equation non-linear, so adding two ECDSA signatures together produces garbage.

&gt; **Caveat:** 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.

### ECDSA malleability

ECDSA has a quirk: for any valid signature `(r, s)`, the pair `(r, n - s)` is also a valid signature for the same message. This is called **signature malleability** - a third party can &quot;flip&quot; your signature without knowing your private key.

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.

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

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

---

## 5. The nonce catastrophe

Everything above assumes one thing: **the nonce `k` is unique and secret for every signature.** If it&apos;s not, you&apos;re gonna have a bad time.

### Nonce reuse in Schnorr

If you sign two different messages `m₁` and `m₂` with the same nonce `k`:

```
s₁ = k + e₁x
s₂ = k + e₂x
```

Subtract:

```
s₁ - s₂ = (e₁ - e₂)x
```

Solve for the private key:

```
x = (s₁ - s₂) / (e₁ - e₂)
```

Two signatures, same nonce, your private key is gone. Simple division.

### Nonce reuse in ECDSA

Same disaster, slightly different algebra:

```
s₁ = k⁻¹(m₁ + rx)
s₂ = k⁻¹(m₂ + rx)
```

Since `k` and `r` are the same (same nonce means same `R`, same `r`):

```
s₁ - s₂ = k⁻¹(m₁ - m₂)
k = (m₁ - m₂) / (s₁ - s₂)
```

Once you have `k`, extract `x`:

```
x = (s₁k - m₁) / r
```

Game over.

### The PS3 hack: a real-world catastrophe

This isn&apos;t theoretical. In 2010, **fail0verflow** demonstrated this exact attack against Sony&apos;s PlayStation 3.

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.

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

With the key, anyone could sign arbitrary code and run it on any PS3. The console&apos;s entire code-signing security model was broken - permanently and irreversibly. Sony couldn&apos;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&apos;t be patched.

### The fix: deterministic nonces (RFC 6979)

The root cause of these catastrophes is relying on a random number generator to produce `k`. If the RNG is broken, biased, or reused, the private key leaks.

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

Most modern ECDSA implementations use RFC 6979 or something equivalent. Schnorr (BIP340) similarly recommends deterministic nonce generation.

---

## Wrapping up

So that&apos;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&apos;t.

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&apos;t like math.

---</content:encoded></item><item><title>Private keys and elliptic curves: a deep-dive for people who don&apos;t like math</title><link>https://luk3.tech/blog/private-keys-elliptic-curves/</link><guid isPermaLink="true">https://luk3.tech/blog/private-keys-elliptic-curves/</guid><description>You don&apos;t need a math degree to understand elliptic curve cryptography. You just need to understand why spilled rice is more secure than a snake.</description><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&gt; If you ever wondered how elliptic curves and private keys actually work, and you never liked math like me - let&apos;s try to get the idea without too much of it.

---

## First: why does any of this exist?

Blockchain needs to solve one problem: **how do you prove you own something without telling anyone your secret?**

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.

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.

---

## 1. What even is a private key?

A private key is just a number. An absurdly large number - on Bitcoin&apos;s curve, it&apos;s a random integer between 1 and roughly:

```
115792089237316195423570985008687907852837564279074904382605163141518161494337
```

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

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

---

## 2. Elliptic curves: a special kind of function

Elliptic curves are the foundation of all this. Think of them as a twisted version of the linear function you know from primary school.

You know linear functions. `y = ax + b`. Grows in a straight line. Boring but useful.

You probably also know quadratics - parabolas and such. Those grow faster, curve around, have some nice properties.

Elliptic curves are a different beast entirely. The general form used in cryptography is called the **Weierstrass form**:

```
y² = x³ + ax + b
```

For Bitcoin specifically (the **secp256k1** curve), `a = 0` and `b = 7`, giving you:

```
y² = x³ + 7
```

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.

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 `y` means both `+y` and `-y` are solutions.

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: **a weird kind of arithmetic that&apos;s hard to reverse**.

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

---

## 3. Point addition: geometry as arithmetic

Here&apos;s where elliptic curves get interesting.

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

**To add point P and point Q:**
1. Draw a straight line through P and Q
2. That line intersects the curve at exactly one more point
3. Reflect that intersection point across the x-axis
4. That reflected point is `P + Q`

This is not addition in any normal sense. No numbers are being summed. You&apos;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.

The interactive below shows this:

&lt;details&gt;
  &lt;summary&gt;The actual formula (you don&apos;t need to learn this)&lt;/summary&gt;

  Given points P = (x₁, y₁) and Q = (x₂, y₂) on the curve:

  ```
  λ = (y₂ - y₁) / (x₂ - x₁)

  x₃ = λ² - x₁ - x₂
  y₃ = λ(x₁ - x₃) - y₁
  ```

  The result P + Q = (x₃, y₃).

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

&lt;/details&gt;

---

## 4. Scalar multiplication: the secret weapon

Now that we can add two points, we can do **scalar multiplication**: multiplying a point by a number `k` means adding it to itself `k` times.

```
k × P = P + P + P + ... (k times)
```

This is the core operation in elliptic curve cryptography. If `G` is a fixed starting point on the curve (called the **generator point** - the same for everyone using secp256k1), and `k` is your private key, then:

```
Public Key = k × G
```

That&apos;s it. Your public key is the result of scalar-multiplying the generator point by your private key.

Now here&apos;s the key question your brain is probably asking: *&quot;If I know G and I know the public key, can&apos;t I just figure out k by reversing the multiplication?&quot;*

And here&apos;s where it gets delicious.

**In principle, yes.** 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 **Discrete Logarithm Problem**.

**In practice, no** - because of finite fields. Which brings us to the twist.

---

## 5. Finite fields: the real story

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

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

### What&apos;s a finite field?

A finite field is a &quot;closed universe&quot; of numbers with specific rules:

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

Think of modular arithmetic like a clock - or like an unsigned integer that overflows. If your variable holds values `0` through `7`, then:

```
4 + 4 = 8 → wraps to 0
4 + 6 = 10 → wraps to 2
7 + 7 = 14 → wraps to 6
```

This is **arithmetic modulo 8** - every result is taken `mod 8`.

### Why this matters for cryptography

Here&apos;s the clever bit. Say someone knows you did some additions modulo 8, and the result is `4`. They can&apos;t tell whether you computed:

- `1 + 3`
- `4 + 0`
- `2 + 2`
- `4 + 8` (one full wrap)
- `4 + 16` (two wraps)
- `4 + 10000000` (a lot of wraps)

**You cannot recover the inputs from the output.** The modular wrapping destroys that information. This is one of the ways to construct trapdoor (one-way) functions in cryptography.

Bitcoin&apos;s prime `p` is:

```
p = 2²⁵⁶ - 2³² - 977
```

That&apos;s not 8 possible values. That&apos;s a number with 77 decimal digits. The wrapping is... extensive.

---

## 6. Elliptic curves over finite fields: the spilled rice

Here&apos;s what happens when you take the elliptic curve equation and restrict it to a finite field:

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

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&apos;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.

This is still technically an &quot;elliptic curve&quot; - the same algebraic structure applies, point addition still works, the formula still holds - but geometrically, it&apos;s unrecognizable.

**Why does the spilled rice matter?**

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

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

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

---

## 7. The full picture: private key → public key → address

Let&apos;s tie it all together.

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

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

**Your public key** is computed as:

```
Public Key = k × G
```

Where `×` is scalar multiplication over the elliptic curve, operating in the finite field GF(p).

Knowing `G` and the public key, recovering `k` requires solving ECDLP - which is computationally infeasible with current (and foreseeable) technology.

**Your address** 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.

The full chain:

```
Random number k (secret)
      ↓  scalar multiplication (one-way)
Public Key (shareable)
      ↓  hashing (one-way)
Address (shareable)
```

Each arrow is a one-way function. None can be reversed. That&apos;s the whole system.

---

## What&apos;s next: digital signatures

Everything above is the foundation for understanding **digital signatures** - 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.

The next post will explain how that signing process works on top of the elliptic curve math we covered here.

---</content:encoded></item><item><title>Math cheatsheet before you deep-dive into ZK</title><link>https://luk3.tech/blog/math-cheatsheet-before-zk/</link><guid isPermaLink="true">https://luk3.tech/blog/math-cheatsheet-before-zk/</guid><description>You don&apos;t need a math degree. You need about six building blocks. Here they are, short and visual.</description><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>We&apos;re not going to explain ZK in five minutes here. We&apos;re not even going to touch it. But there are a handful of math topics you **have to** understand before anything in this series makes sense.

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&apos;s get comfortable with it slowly, one concept at a time, with zero greek letters and some things you can click on.

---

## 1. Modular arithmetic

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

That&apos;s modular arithmetic. The `mod` operator gives you the remainder after division:

```
10 + 5 = 15 → 15 mod 12 = 3
7 + 7 = 14 → 14 mod 12 = 2
```

In crypto, the modulus isn&apos;t 12. It&apos;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.

Try it yourself. The clock below uses a small modulus so you can watch values wrap around:

&lt;small&gt;Formal explanation: [Modular arithmetic on Wikipedia](https://en.wikipedia.org/wiki/Modular_arithmetic)&lt;/small&gt;

---

## 2. Finite fields

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.

More precisely, a finite field **GF(p)** is the set of integers `{0, 1, 2, ..., p-1}` where `p` is prime, and all arithmetic is done mod `p`. Three properties matter:

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

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;small&gt;Formal explanation: [Finite field on Wikipedia](https://en.wikipedia.org/wiki/Finite_field)&lt;/small&gt;

---

## 3. The discrete logarithm problem

Here&apos;s the core asymmetry that makes public-key cryptography possible.

**The easy direction**: Given a base `g`, an exponent `k`, and a modulus `p`, computing `g^k mod p` is fast. Computers are good at exponentiation.

```
g = 3, k = 5, p = 7
3^5 = 243 → 243 mod 7 = 5
```

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

This is the **discrete logarithm problem (DLP)**. &quot;Discrete&quot; because you&apos;re working in a finite field (discrete values, not continuous). &quot;Logarithm&quot; because you&apos;re trying to find the exponent.

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

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

&lt;small&gt;Formal explanation: [Discrete logarithm on Wikipedia](https://en.wikipedia.org/wiki/Discrete_logarithm)&lt;/small&gt;

---

## 4. Hash functions

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&apos;t matter if your input is four letters or four gigabytes.

But how? How does the word `math` turn into `a0885e289f3e77a14e06e6887a1fc93b5ed2e14cfe7f7052805fe92e1a0e0e38`?

### What actually happens inside a hash

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

**Step 1: Convert to binary.** Your input becomes raw bytes. The string `math` becomes four ASCII values: `109 97 116 104`, which in binary is `01101101 01100001 01110100 01101000`.

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

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

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

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

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

**Step 5: Output.** The final internal state (or a portion of it) becomes your hash.

The result is deterministic (same input, same output, every time) but practically irreversible. You can&apos;t reconstruct `math` from the hash any more than you can reconstruct an egg from an omelette. This makes hash functions a kind of **trapdoor**: easy to compute forward, impossible to reverse. You&apos;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;small&gt;Formal explanation: [Cryptographic hash function on Wikipedia](https://en.wikipedia.org/wiki/Cryptographic_hash_function) · [Trapdoor function on Wikipedia](https://en.wikipedia.org/wiki/Trapdoor_function)&lt;/small&gt;

---

## 5. Constraints: how ZK thinks about computation

Here&apos;s where things start to feel unfamiliar.

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 **set of equations** (called constraints) that must all be satisfied simultaneously.

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

```
a * b = 35
```

If you can provide values for `a` and `b` that satisfy this equation, you&apos;ve proven you know them. The constraint system doesn&apos;t care *how* you found the values. It only cares that they satisfy the equations.

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 **R1CS** (Rank-1 Constraint System), where every constraint has the form:

```
(left) * (right) = (output)
```

Each of `left`, `right`, and `output` is a linear combination of variables. The entire program becomes a system of these equations.

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

&lt;small&gt;Formal explanation: [R1CS on Wikipedia](https://en.wikipedia.org/wiki/Rank-1_constraint_system)&lt;/small&gt;

---

## 6. The idea behind zero-knowledge proofs

Now you have all the pieces. ZK proofs combine the concepts above into a single protocol.

The setup: there&apos;s a **prover** (who knows a secret) and a **verifier** (who wants to be convinced the prover knows it, without learning the secret).

A zero-knowledge proof has three properties:

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

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.

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 &quot;correct&quot; means, and ZK proofs let you demonstrate correctness without revealing your inputs.

&lt;small&gt;Formal explanation: [Zero-knowledge proof on Wikipedia](https://en.wikipedia.org/wiki/Zero-knowledge_proof)&lt;/small&gt;

---</content:encoded></item></channel></rss>